From 78a42464d54584317f25f9c5247ae0468b1316f6 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 22 Feb 2022 20:21:39 +0300 Subject: [PATCH 01/60] Create QueryBuilder.php --- src/QueryBuilder.php | 360 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 src/QueryBuilder.php diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php new file mode 100644 index 0000000..1f6694a --- /dev/null +++ b/src/QueryBuilder.php @@ -0,0 +1,360 @@ + + */ +namespace Evas\Orm; + +use Evas\Db\Builders\QueryBuilder as DbQueryBuilder; +use Evas\Db\Interfaces\DatabaseInterface; +// use Evas\Orm\Model; +use Evas\Orm\ActiveRecord; +use Evas\Orm\Relation; + +class QueryBuilder extends DbQueryBuilder +{ + /** @var string разделитель для полей связанных записей */ + public static $relationDataSeparator = '_-_'; + + /** @var string класс модели данных */ + protected $model; + + /** @var array обработанный результат запроса в виде моделей */ + protected $result = []; + + + /** + * Расширяем конструктор передачей класса модели. + * @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 + */ + public function fromModel(string $model) + { + $this->model = $model; + $this->from($model::tableName()); + return $this; + } + + /** + * Преобразование полученых записей. + * @param array|null записи в виде массива + * @return array записи в виде моделей + */ + private function prepareGetResult(array $rows): array + { + foreach ($rows as $row) { + $this->prepareOneResult($row); + } + return array_values($this->result); + } + + /** + * Преобразование полученой записи. + * @param array|null запись в виде массива + * @return ActiveRecord запись в виде модели + */ + private function prepareOneResult(array $row = null) + { + if (!$row) return $row; + $foreigns = []; + if (count($this->withs) > 0) { + foreach ($row as $key => $value) { + @[$fname, $fkey] = explode(static::$relationDataSeparator, $key, 2); + if ($fkey && in_array($fname, array_keys($this->withs))) { + $foreigns[$fname][$fkey] = $value; + unset($row[$key]); + } + } + } + $id = $row[$this->model::primaryKey()]; + if (!isset($this->result[$id])) { + $this->result[$id] = new $this->model($row); + } + $result = $this->result[$id]; + foreach ($foreigns as $fname => $subs) { + $this->withs[$fname][0]->addRelated($result, $subs); + } + return $result; + } + + /** + * Выполнение select-запроса с получением результирующих строк в виде массива моделей. + * @param array|null столбцы + * @return array модели + */ + public function get($columns = null): ?array + { + $this->applyWiths(); + $rows = parent::get($columns); + return (count($this->aggregates) < 1 && count($this->groups) < 1 && count($this->withs) >= count($this->joins)) + ? $this->prepareGetResult($rows) + : $rows; + } + + /** + * Выполнение select-запроса с получением результирующей строки в виде модели. + * @param array|null столбцы + * @return ActiveRecord|null + */ + public function one($columns = null): ?ActiveRecord + { + if (count($this->withs)) { + $rows = $this->get($columns); + return count($rows) ? $rows[0] : null; + } + $row = parent::one($columns); + return $row ? $this->prepareOneResult($row) : null; + } + + protected function applyWiths() + { + if (count($this->withs)) { + $columns = static::prepareModelColumns($this->columns, $this->model); + $this->addSelect($columns); + foreach ($this->withs as [$relation, $columns, $query]) { + $this->applyWith($relation, $columns, $query); + } + } + } + + protected function applyWith(Relation $relation, array $columns = null, self $query = null) + { + $columns = static::prepareModelColumns($columns, $relation->foreignModel, $relation->name); + $this->addSelect($columns); + $this->leftJoinSub($query ?? $relation->foreignTable, $relation->name, $relation->foreignFullKey, $relation->localFullKey); + return $this; + } + + protected $withs = []; + + protected function addWith(string $name, array $columns = null, self $query = null) + { + $relation = $this->getRelation($name); + if ($relation) { + if (!isset($this->withs[$relation->name])) { + $this->withs[$relation->name] = [$relation, $columns, $query]; + } + } + return $this; + } + + public function with(...$props) + { + foreach ($props as &$prop) { + if (is_string($prop)) { + @list($name, $columns) = explode(':', $prop); + $this->addWith($name, $columns); + + } else if (is_array($prop)) { + foreach ($prop as $name => &$sub) { + $columns = null; + $query = null; + if (is_string($name)) { + if (is_string($sub)) { + $columns = $sub; + } else if (is_callable($sub)) { + @list($name, $columns) = explode(':', $name); + $relation = $this->getRelation($name); + if (!$relation) continue; + $query = new static($this->db, $relation->foreignModel); + $sub($query); + } + } else if (is_string($sub)) { + @list($name, $columns) = explode(':', $sub); + } else { + throw new \InvalidArgumentException('Incorrect with syntax'); + } + $this->addWith($name, $columns, $query); + } + } + } + return $this; + } + + protected function addHas( + bool $isNot, bool $isWith, string $relationName, $operator = null, $value = null + ) { + @[$relationName, $columns] = explode(':', $relationName); + $relation = $this->getRelation($relationName); + if ($relation) { + $selfColumns = static::prepareModelColumns($this->columns, $this->model); + $this->select($selfColumns); + if ($isNot) { + $this->leftOuterJoinSub($relation->foreignTable, $relation->name, $relation->foreignFullKey, $relation->localFullKey); + $this->whereNull($relation->foreignFullKey); + } else { + if (func_num_args() > 3) { + if ($this->isQueryable($operator)) { + if ($operator instanceof \Closure) { + $cb = $operator; + $cb($operator = $this->forSubQuery()); + } + foreach ($operator->wheres as $where) { + if (isset($where['columns'])) foreach($where['columns'] as &$column) { + $parts = explode('.', $column); + if (count($parts) < 2 || $parts[0] != $relationName) { + array_unshift($parts, $relation); + $column = implode('.', $parts); + } + } + $this->pushWhere($where['type'], $where); + } + $this->addBindings('where', $operator->getBindings('where')); + } else { + [$value, $operator] = $this->prepareValueAndOperator( + $value, $operator, func_num_args() === 4 + ); + } + if ($columns) { + $columns = explode(',', $columns); + foreach ($columns as $column) { + $column = $relation->name . '.' . $column; + // $this->count($column); + // $this->havingAggregate('count', $column, $operator, $value); + $this->where($column, $operator, $value); + // $this->whereNotNull($column); + // $this->where($column, '!=', 0); + } + } else { + $foreignFullPrimary = $relation->name . '.' . $relation->foreignModel::primaryKey(); + $this->count($foreignFullPrimary); + $this->havingAggregate('count', $foreignFullPrimary, $operator, $value); + } + } else if ($columns) { + $columns = explode(',', $columns); + foreach ($columns as $column) { + $column = $relation->name . '.' . $column; + // $this->count($column); + // $this->havingAggregate('count', $column, $operator, $value); + // $this->where($column, $operator, $value); + $this->whereNotNull($column); + // $this->where($column, '!=', 0); + } + // $columns = static::prepareModelColumns($columns, $relation->foreignModel, $relation->name); + } + $this->joinSub($relation->foreignTable, $relation->name, $relation->foreignFullKey, $relation->localFullKey); + $this->groupBy($relation->localFullKey); + } + } + return $this; + } + + public function has(string $relationName, $operator = null, $value = null) + { + return $this->addHas(false, false, ...func_get_args()); + } + + public function notHas(string $relationName, $operator = null, $value = null) + { + return $this->addHas(true, false, ...func_get_args()); + } + + public function withHas(string $relationName, $operator = null, $value = null) + { + return $this->addHas(false, true, ...func_get_args()); + } + + + protected function getRelation(string $name): ?Relation + { + return $this->model::getRelation($name); + } + + // protected function parseTableWithColumns(string $value) + // { + // @list($table, $columns) = explode(':', $value); + // return [$table, $columns]; + // } + + protected static function prepareModelColumns($columns, string $model, string $asPrefix = null) + { + $columns = static::prepareColumns($columns); + if (!$columns) $columns = $model::columns(); + $table = $model::tableName(); + $keys = []; + foreach ($columns as &$column) { + if (!empty($asPrefix)) { + $as = $asPrefix . static::$relationDataSeparator . $column; + $col = "{$asPrefix}.{$column}"; + } else { + $as = $column; + $col = "{$table}.{$column}"; + } + $keys[$as] = $col; + } + return $keys; + } + + protected static function prepareColumns($columns): ?array + { + return (empty($columns) || $columns = '*' || (is_array($columns) && in_array('*', $columns))) + ? null + : explode(',', str_replace(' ', '', $columns)); + } + + // public function with(...$props) + // { + // // $relations = []; + // foreach ($props as &$prop) { + // if (is_string($prop)) { + // @list($name, $columns) = explode(':', $prop); + // $columns = $this->prepareColumns($columns) + // $relation = $this->model->$name(); + // // $this->base->leftJoin($relation->name) + // // $sql = 'SELECT * FROM `user`' + // // . ' LEFT JOIN `group` ON `group`.`user_id` = `user`.`id`'; + // // $relation = $this->getRelation($name); + // } else if (is_array($prop)) { + // foreach ($prop as $name => &$sub) { + // $columns = null; + // if (is_string($name)) { + // if (is_string($sub)) { + // $columns = $sub; + // } else if (is_callable($sub)) { + // @list($name, $columns) = $name; + // $relation = $this->model->$name(); + // $sub(new static($relation)); + // } + // } else if (is_string($sub)) { + // @list($name, $columns) = explode(':', $sub); + // } else { + // throw new \Exception('Incorrect!'); + // } + // $columns = $this->prepareColumns($columns); + // $relation = $this->model->$name(); + // // $relation = $this->getRelation($name); + // } + // } + // } + // } + + public function withCount(...$props) + {} + + public function withSum(...$props) + {} + + public function withMax(...$props) + {} + + public function withMin(...$props) + {} + + public function withAvg(...$props) + {} + + public function withExists(...$props) + {} +} From 3e1feeb1fa5be7dceda33f358cd971f224d483a7 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 22 Feb 2022 20:21:49 +0300 Subject: [PATCH 02/60] Create RelatedCollection.php --- src/RelatedCollection.php | 93 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/RelatedCollection.php diff --git a/src/RelatedCollection.php b/src/RelatedCollection.php new file mode 100644 index 0000000..10e89fb --- /dev/null +++ b/src/RelatedCollection.php @@ -0,0 +1,93 @@ + + */ +namespace Evas\Orm; + +use Evas\Base\Help\Collection; +use Evas\Base\Help\PhpHelp; +use Evas\Orm\ActiveRecord; +use Evas\Orm\Relation; + +class RelatedCollection extends Collection +{ + /** @var ActiveRecord модель */ + protected $model; + /** @var string связь */ + 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)) { + $item = new $this->relation->foreignModel($item); + } + if (!($id = $item->primaryValue()) || !$this->has($id)) { + $this->ids[] = $id; + return parent::add($item); + } + return $this; + } + + /** + * Конструктор. + * @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; + } +} From 7b9dea28ce28d96ca7209e29a0b996b941ae074c Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 22 Feb 2022 20:21:54 +0300 Subject: [PATCH 03/60] Create Relation.php --- src/Relation.php | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/Relation.php diff --git a/src/Relation.php b/src/Relation.php new file mode 100644 index 0000000..f0b91be --- /dev/null +++ b/src/Relation.php @@ -0,0 +1,92 @@ + + */ +namespace Evas\Orm; + +use Evas\Orm\ActiveRecord; + +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); + if (!$localKey) $localKey = $localModel::primaryKey(); + $this->type = $type; + $this->localModel = $localModel; + $this->localKey = $localKey; + $this->foreignModel = $foreignModel; + $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 (strlen($pk) > 1 && strrpos($pk, 's') == strlen($pk) - 1) { + $pk = substr($pk, 0, strlen($pk) - 1); + } + $pk .= '_id'; + } + return $pk; + } + + /** + * Установка имени связей модели. + * @param string имя + * @return self + */ + public function setName(string $name) + { + $this->name = $name; + $this->foreignFullKey = "$this->name.$this->foreignKey"; + return $this; + } + + /** + * Добавление связи с моделью. + * @param ActiveRecord модель + * @param array данные внешней модели + * @return ActiveRecord модель + */ + public function addRelated(ActiveRecord $model, array $foreignData) + { + return $model->addRelated($this->name, new $this->foreignModel($foreignData)); + } +} From 15ddcc1898da9fccf67c620dd9d2ee8f0b9f8820 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 22 Feb 2022 20:22:02 +0300 Subject: [PATCH 04/60] Create RelationsTrait.php --- src/Traits/RelationsTrait.php | 131 ++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/Traits/RelationsTrait.php diff --git a/src/Traits/RelationsTrait.php b/src/Traits/RelationsTrait.php new file mode 100644 index 0000000..69e6d7f --- /dev/null +++ b/src/Traits/RelationsTrait.php @@ -0,0 +1,131 @@ + + */ +namespace Evas\Orm\Traits; + +use Evas\Base\Help\PhpHelp; +use Evas\Orm\ActiveRecord; +use Evas\Orm\OrmException; +use Evas\Orm\RelatedCollection; +use Evas\Orm\Relation; + +trait RelationsTrait +{ + /** @static array связи */ + protected static $relations; + /** @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); + if ($relation->type === 'hasOne') { + 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; + } + + /** + * Инициализация связей. + * @throws \InvalidArgumentException + */ + protected static function initRelations() + { + $relations = static::relations(); + if ($relations) foreach ($relations as $name => &$relation) { + if (!$relation instanceof Relation) { + throw new \InvalidArgumentException(sprintf( + 'Relation must be instance of %s, %s given', + Relation::class, PhpHelp::getType($relation) + )); + } + $relation->setName($name); + } + static::$relations = &$relations; + } + + /** + * Получение связи по имени. + * @param string имя связи + * @return Relation|null + */ + public static function getRelation(string $name): ?Relation + { + if (is_null(static::$relations)) { + static::initRelations(); + } + return static::$relations[$name] ?? null; + } + + /** + * Проверка наличия связи. + * @param string имя связи + * @return bool + */ + public static function hasRelation(string $name): bool + { + if (is_null(static::$relations)) { + static::initRelations(); + } + return isset(static::$relations[$name]); + } + + /** + * Установка множественной связи. + * @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); + } +} From 5fde1f109335237f2de59315850c3b96b2cae9c1 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 22 Feb 2022 20:22:15 +0300 Subject: [PATCH 05/60] Update ActiveRecord.php --- src/ActiveRecord.php | 361 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 312 insertions(+), 49 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 66b3b3a..5690f7e 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -6,25 +6,37 @@ */ namespace Evas\Orm; +use \JsonSerializable; 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\RelatedCollection; +use Evas\Orm\Traits\RelationsTrait; +use Evas\Orm\QueryBuilder; -abstract class ActiveRecord +abstract class ActiveRecord implements JsonSerializable { // подключаем поддержку произвольных хуков в наследуемых классах use HooksTrait; - /** @var string имя соединения с базой данных*/ + // подключаем поддержку связей + use RelationsTrait; + + // подключаем методы сборщика запросов + // use ActiveRecordBuilderTrait; + + /** @var string имя соединения с базой данных */ public static $dbname; /** @var string имя соединения с базой данных только для записи */ public static $dbnameWrite; - /*** @var string кастомное имя таблицы */ + /** @var string кастомное имя таблицы */ public static $tableName; + /** @var array данные модели */ + protected $modelData = []; + /** * Получение соединения с базой данных. * @param bool использовать ли соединение для записи @@ -54,15 +66,11 @@ public static function getDbName(bool $write = false): ?string */ public static function generateTableName(): string { - $className = get_called_class(); + $className = static::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'; } @@ -168,6 +176,15 @@ public function fill(array $props): ActiveRecord return $this; } + /** + * Получение значения первичного ключа модели. + * @return int|string|null + */ + public function primaryValue() + { + return $this->{static::primaryKey()}; + } + /** * Получение маппинга данных записи для базы данных. @@ -191,12 +208,12 @@ public function getRowProperties(): array public function getState(): array { $state = static::getDb()->identityMapGetState($this, static::primaryKey()); - foreach ($state as $name => &$value) { + if ($state) foreach ($state as $name => &$value) { if (!isset($value) || !in_array($name, static::columns())) { unset($state[$name]); } } - return $state; + return $state ?? []; } /** @@ -206,8 +223,7 @@ public function getState(): array public function getUpdatedProperties(): array { $props = $this->getRowProperties(); - $pk = static::primaryKey(); - if (empty($this->$pk)) { + if (empty($this->primaryValue())) { return $props; } else { // $state = static::getDb()->identityMapGetState($this, $pk); @@ -246,14 +262,28 @@ public function save() $this->hook('afterInsert'); } else { $this->hook('beforeUpdate'); - static::getDb(true)->update(static::tableName(), $this->getUpdatedProperties()) - ->where("$pk = ?", [$this->$pk])->one(); + static::getDb(true)->table(static::tableName()) + ->where($pk, $this->$pk)->update($this->getUpdatedProperties()); $this->hook('afterUpdate'); } $this->hook('afterSave'); return static::getDb()->identityMapUpdate($this, $pk); } + /** + * Сохранение модели и её связей. + */ + public function push() + { + $this->save(); + foreach ($this->relatedCollections as $models) { + if (!is_array($models)) $models = [$models]; + foreach ($models as $model) { + $model->push(); + } + } + } + /** * Удаление записи. */ @@ -265,8 +295,8 @@ public function delete() return $this; } $this->hook('beforeDelete'); - $qr = static::getDb(true)->delete(static::tableName()) - ->where("$pk = ?", [$this->$pk])->one(); + $qr = static::getDb(true)->table(static::tableName()) + ->where($pk, $this->$pk)->limit(1)->delete(); $this->hook('afterDelete', $qr->rowCount()); if (0 < $qr->rowCount()) { static::getDb()->identityMapUnset($this, $pk); @@ -275,6 +305,16 @@ public function delete() return $this; } + /** + * Обновление данных модели из базы. + */ + public function reload() + { + $pv = $this->primaryValue(); + if (!$pv) return; + $this->fill(static::find($pv)); + } + /** * Создание модели записи. * @param array|null значения записи @@ -296,54 +336,277 @@ public static function insert(array $props = null): ActiveRecord } /** - * Поиск записи через сборщик запроса. - * @param string|null столбцы - * @return QueryBuilderInterface + * Поиск записи/записей. + * @param array|int столбец или столбцы + * @return static|static[] */ - public static function find(string $columns = null): QueryBuilderInterface + public static function find($ids) { - return static::getDb()->select(static::tableName(), $columns); + $ids = is_array($ids) ? $ids : func_get_args(); + $db = static::getDb(); + return (new QueryBuilder($db, static::class))->find($ids); } /** - * Поиск по первичному ключу. - * @param int|string значение первичного ключа, перечисление - * @return static|array of static + * Поиск по sql-запросу. + * @param string sql-запрос + * @param array|null значения запроса для экранирования + * @return static|static[] */ - public static function findByPK(...$ids) + public static function query(string $sql, array $values = null) { - $pk = static::primaryKey(); - $qb = static::find(); - if (count($ids) > 1) { - return $qb->whereIn("`$pk`", $ids) - ->query(count($ids))->classObjectAll(static::class); + $qr = static::getDb()->query($sql, $values); + return strstr($qr->stmt()->queryString, 'LIMIT 1') + ? $qr->object(static::class) + : $qr->objectAll(static::class); + } + + /** + * Получение или создание новой записи. + * @param array свойства поиска + * @param array свойства вставки + * @return static + */ + public static function firstOrCreate(array $findProps, array $createProps = null) + { + $keys = array_keys($findProps); + $vals = array_values($findProps); + foreach (static::columns() as $i => $key) { + if (!in_array($key, $keys)) { + unset($keys[$i]); + unset($vals[$i]); + } + } + $record = static::whereRowValues($keys, $vals)->one(); + if ($record) return $record; + return static::create(array_merge($findProps, $createProps)); + } + + /** + * Проброс сборщика запросов через магический вызов статического метода. + * @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::getDb(); + return (new QueryBuilder($db, static::class))->$name(...$args); + } + throw new \BadMethodCallException(sprintf( + 'Call to undefined method %s::%s()', __CLASS__, $name + )); + } + + /** + * Описание связей модели. + * @return array + */ + protected static function relations(): array + { + return []; + } + + /** + * Подгрузка связанных записей. + * @param string имя связи + * @param array|null столбцы + * @return self + */ + public function loadRelated(string $name, array $columns = null, QueryBuilder $query = null) + { + $relation = static::getRelation($name); + if ($query) { + $qb = $query->where($relation->foreignKey, $this->{$relation->localKey}); + } else { + $qb = $relation->foreignModel::where($relation->foreignKey, $this->{$relation->localKey}); + } + if ($relation->type === 'hasOne') $qb->limit(1); + $models = $qb->get($columns); + $this->setRelatedCollection($relation->name, $models); + return $this; + } + + public function load(...$props) + { + foreach ($props as &$prop) { + if (is_string($prop)) { + @list($name, $columns) = explode(':', $prop); + $this->loadRelated($name, $columns); + } else if (is_array($prop)) { + foreach ($prop as $name => &$sub) { + $columns = null; + $query = null; + if (is_string($name)) { + if (is_string($sub)) { + $columns = $sub; + } else if (is_callable($sub)) { + @list($name, $columns) = explode(':', $name); + $relation = $this->getRelation($name); + if (!$relation) continue; + $db = static::getDb(); + $query = new QueryBuilder($db, $relation->foreignModel); + $sub($query); + } + } else if (is_string($sub)) { + @list($name, $columns) = explode(':', $sub); + } else { + throw new \InvalidArgumentException('Incorrect load syntax'); + } + $this->loadRelated($name, $columns, $query); + } + } + } + } + + /** + * Получение данных модели. + * @return array + */ + public function getData(): array + { + return $this->modelData; + } + + /** + * Получение всех связанных записей модели. + * @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; + } + + /** + * Установка свойства. + * @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 $qb->where("`$pk` = ?", $ids) - ->one()->classObject(static::class); + return $this->modelData[$name] ?? null; } } /** - * Поиск по id, алиас для findByPK. - * @param int id, перечисление - * @return static|array of static + * Проверка наличия свойства. + * @param string имя свойства + * @return bool */ - public static function findById(int ...$ids) - { - return static::findByPK(...$ids); - } + public function __isset(string $name): bool + { + return isset($this->modelData[$name]); + } /** - * Поиск по sql-запросу. - * @param string sql-запрос - * @param array|null значения запроса для экранирования - * @return static|static[] + * Очистка свойства. + * @param string имя свойства */ - public static function query(string $sql, array $values = null) + public function __unset(string $name) { - $qr = static::getDb()->query($sql, $values); - return strstr($qr->stmt()->queryString, 'LIMIT 1') - ? $qr->classObject(static::class) - : $qr->classObjectAll(static::class); + unset($this->modelData[$name]); } + + // Конвертация + + /** + * Конвертация данных и связей модели в массив. + * @return array + */ + public function toArray(): array + { + 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(); + } + + // protected static $list = []; + // protected static $map = []; + + // public static function list(): array + // { + // return static::$list; + // } + + // public static function map(): array + // { + // return static::$map; + // } + + // public static function size(): int + // { + // return count(static::$map); + // } + + // public static function getCached($id = null) + // { + // if (null === $id) { + // return static::list(); + // } else if (is_array($id)) { + // $result = []; + // foreach ($id as $sub) { + // $result[] = static::getCached($sub); + // } + // return $result; + // } else { + // return static::$map[$id]; + // } + // } } From b0c0a555c32f0650a19bf567bd4814080fd750dc Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 6 Mar 2022 04:05:22 +0300 Subject: [PATCH 06/60] Update ActiveRecord.php --- src/ActiveRecord.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 5690f7e..330c920 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -24,9 +24,6 @@ abstract class ActiveRecord implements JsonSerializable // подключаем поддержку связей use RelationsTrait; - // подключаем методы сборщика запросов - // use ActiveRecordBuilderTrait; - /** @var string имя соединения с базой данных */ public static $dbname; /** @var string имя соединения с базой данных только для записи */ @@ -342,7 +339,7 @@ public static function insert(array $props = null): ActiveRecord */ public static function find($ids) { - $ids = is_array($ids) ? $ids : func_get_args(); + $ids = func_num_args() > 1 ? func_get_args() : $ids; $db = static::getDb(); return (new QueryBuilder($db, static::class))->find($ids); } From 392997de863e15120e6b942ccc0914d8976c4625 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 6 Mar 2022 04:07:30 +0300 Subject: [PATCH 07/60] Update QueryBuilder.php --- src/QueryBuilder.php | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 1f6694a..0d352e3 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -96,9 +96,13 @@ private function prepareOneResult(array $row = null) */ public function get($columns = null): ?array { + $this->result = []; $this->applyWiths(); $rows = parent::get($columns); - return (count($this->aggregates) < 1 && count($this->groups) < 1 && count($this->withs) >= count($this->joins)) + return ( + count($this->aggregates) < 1 && count($this->groups) < 1 + && count($this->withs) >= count($this->joins) + ) ? $this->prepareGetResult($rows) : $rows; } @@ -110,17 +114,28 @@ public function get($columns = null): ?array */ public function one($columns = null): ?ActiveRecord { + $this->result = []; if (count($this->withs)) { - $rows = $this->get($columns); + $rows = $this->limit(1)->get($columns); return count($rows) ? $rows[0] : null; } $row = parent::one($columns); - return $row ? $this->prepareOneResult($row) : null; + return $this->prepareOneResult($row); } protected function applyWiths() { if (count($this->withs)) { + if ($this->limit) { + // $this->fromSub(function ($query) { + // $query->from($this->from)->limit($this->limit); + // }, $this->from); + // $this->fromSub($this->getSql(), $this->from); + $this->fromSub($this, $this->from); + $this->bindings['where'] = []; + $this->wheres = []; + $this->limit(null); + } $columns = static::prepareModelColumns($this->columns, $this->model); $this->addSelect($columns); foreach ($this->withs as [$relation, $columns, $query]) { From 2426b39e8027754d373b7494198df615a248e4c9 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sat, 18 Jun 2022 19:01:42 +0700 Subject: [PATCH 08/60] ActiveRecord: fix data types --- src/ActiveRecord.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 330c920..38d6c43 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -297,7 +297,7 @@ public function delete() $this->hook('afterDelete', $qr->rowCount()); if (0 < $qr->rowCount()) { static::getDb()->identityMapUnset($this, $pk); - $this->id = null; + $this->$pk = null; } return $this; } @@ -309,7 +309,7 @@ public function reload() { $pv = $this->primaryValue(); if (!$pv) return; - $this->fill(static::find($pv)); + $this->fill((array) static::find($pv)); } /** From 642f505abec4a2da1dcdc4a75de5a552d55f5123 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sat, 18 Jun 2022 19:02:37 +0700 Subject: [PATCH 09/60] ActiveRecord: add force insert with not empty primary key --- src/ActiveRecord.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 38d6c43..fa46055 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -213,6 +213,15 @@ public function getState(): array return $state ?? []; } + /** + * Проверка наличия значения первичного ключа записи в состоянии. + * @return bool + */ + protected function hasPrimaryValueInState(): bool + { + return isset($this->getState()[static::primaryKey()]); + } + /** * Получение маппинга измененных свойств записи. * @return array @@ -249,7 +258,12 @@ public function save() $this->hook('beforeSave'); $pk = static::primaryKey(); - if (empty($this->$pk)) { + if ($this->hasPrimaryValueInState()) { + $this->hook('beforeUpdate'); + static::getDb(true)->table(static::tableName()) + ->where($pk, $this->$pk)->update($this->getUpdatedProperties()); + $this->hook('afterUpdate'); + } else { $this->hook('beforeInsert'); static::getDb(true)->insert(static::tableName(), $this->getUpdatedProperties()); $this->$pk = static::lastInsertId(); @@ -257,11 +271,6 @@ public function save() throw new LastInsertIdUndefinedException(); } $this->hook('afterInsert'); - } else { - $this->hook('beforeUpdate'); - static::getDb(true)->table(static::tableName()) - ->where($pk, $this->$pk)->update($this->getUpdatedProperties()); - $this->hook('afterUpdate'); } $this->hook('afterSave'); return static::getDb()->identityMapUpdate($this, $pk); From 4243fe6cf5c67b245cf718bc70204193c141f03e Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Thu, 3 Nov 2022 05:47:34 +0300 Subject: [PATCH 10/60] Clear old --- src/ActiveRecord.php | 618 ---------------------------------- src/QueryBuilder.php | 375 --------------------- src/RelatedCollection.php | 93 ----- src/Relation.php | 92 ----- src/Traits/RelationsTrait.php | 131 ------- 5 files changed, 1309 deletions(-) delete mode 100644 src/ActiveRecord.php delete mode 100644 src/QueryBuilder.php delete mode 100644 src/RelatedCollection.php delete mode 100644 src/Relation.php delete mode 100644 src/Traits/RelationsTrait.php diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php deleted file mode 100644 index fa46055..0000000 --- a/src/ActiveRecord.php +++ /dev/null @@ -1,618 +0,0 @@ - - */ -namespace Evas\Orm; - -use \JsonSerializable; -use Evas\Base\App; -use Evas\Base\Help\HooksTrait; -use Evas\Db\Interfaces\DatabaseInterface; -use Evas\Db\Table; -use Evas\Orm\Exceptions\LastInsertIdUndefinedException; -use Evas\Orm\RelatedCollection; -use Evas\Orm\Traits\RelationsTrait; -use Evas\Orm\QueryBuilder; - -abstract class ActiveRecord implements JsonSerializable -{ - // подключаем поддержку произвольных хуков в наследуемых классах - use HooksTrait; - - // подключаем поддержку связей - use RelationsTrait; - - /** @var string имя соединения с базой данных */ - public static $dbname; - /** @var string имя соединения с базой данных только для записи */ - public static $dbnameWrite; - /** @var string кастомное имя таблицы */ - public static $tableName; - - /** @var array данные модели */ - protected $modelData = []; - - /** - * Получение соединения с базой данных. - * @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); - } - - /** - * Получение имени соединения с базой данных. - * @param bool использовать ли соединение для записи - * @return string|null - */ - public static function getDbName(bool $write = false): ?string - { - return true === $write && !empty(static::$dbnameWrite) - ? static::$dbnameWrite : static::$dbname; - } - - /** - * Генерация имени таблицы из имени класса. - * @return string - */ - public static function generateTableName(): string - { - $className = static::class; - $lastSlash = strrpos($className, '\\'); - if ($lastSlash > 0) { - $className = substr($className, $lastSlash + 1); - } - return strtolower(preg_replace('/([a-z0-9]+)([A-Z]{1})/', '$1_$2', $className)) . 's'; - } - - /** - * Получение имени таблицы из маппинга моделей таблиц. - * @return string|null имя таблицы - */ - public static function tableNameFromMap(): ?string - { - return static::getDb()->modelTablesMap()->getModelTable(get_called_class()); - } - - /** - * Получение имени таблицы. - * @return string - */ - public static function tableName(): string - { - if (empty(static::$tableName)) { - static::$tableName = static::generateTableName(); - } - return static::$tableName; - } - - /** - * Получение объекта таблицы. - * @return Table - */ - public static function table(): Table - { - return static::getDb()->table(static::tableName()); - } - - /** - * Получение первичного ключа. - * @param bool|null переполучить из схемы заново - * @return string - */ - public static function primaryKey(bool $reload = false): string - { - return static::table()->primaryKey($reload); - } - - /** - * Получение столбцов таблицы. - * @param bool|null переполучить из схемы заново - * @return array - */ - public static function columns(bool $reload = false): array - { - return static::table()->columns($reload); - } - - /** - * Получение id последней записи. - * @return int|null - */ - public static function lastInsertId(): ?int - { - return static::table()->lastInsertId(); - } - - /** - * Дефолтные значения новой записи. - * @return array - */ - public static function default(): array - { - return []; - } - - /** - * Конструктор. - * @param array|null свойства модели - */ - public function __construct(array $props = null) - { - $this->hook('beforeConstruct', $props); - - $pk = static::primaryKey(); - $creating = empty($props[$pk]); - - if ($creating) $this->hook('beforeCreate', $props); - else $this->hook('beforeGet', $props); - - if (!empty($props)) $this->fill($props); - - if ($creating) $this->hook('afterCreate'); - else $this->hook('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 int|string|null - */ - public function primaryValue() - { - return $this->{static::primaryKey()}; - } - - - /** - * Получение маппинга данных записи для базы данных. - * @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()); - if ($state) foreach ($state as $name => &$value) { - if (!isset($value) || !in_array($name, static::columns())) { - unset($state[$name]); - } - } - return $state ?? []; - } - - /** - * Проверка наличия значения первичного ключа записи в состоянии. - * @return bool - */ - protected function hasPrimaryValueInState(): bool - { - return isset($this->getState()[static::primaryKey()]); - } - - /** - * Получение маппинга измененных свойств записи. - * @return array - */ - public function getUpdatedProperties(): array - { - $props = $this->getRowProperties(); - if (empty($this->primaryValue())) { - 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 - * @throws LastInsertIdUndefinedException - */ - public function save() - { - if (empty($this->getUpdatedProperties())) { - $this->hook('nothingSave'); - return $this; - } - - $this->hook('beforeSave'); - $pk = static::primaryKey(); - if ($this->hasPrimaryValueInState()) { - $this->hook('beforeUpdate'); - static::getDb(true)->table(static::tableName()) - ->where($pk, $this->$pk)->update($this->getUpdatedProperties()); - $this->hook('afterUpdate'); - } else { - $this->hook('beforeInsert'); - static::getDb(true)->insert(static::tableName(), $this->getUpdatedProperties()); - $this->$pk = static::lastInsertId(); - if (empty($this->$pk)) { - throw new LastInsertIdUndefinedException(); - } - $this->hook('afterInsert'); - } - $this->hook('afterSave'); - return static::getDb()->identityMapUpdate($this, $pk); - } - - /** - * Сохранение модели и её связей. - */ - public function push() - { - $this->save(); - foreach ($this->relatedCollections as $models) { - if (!is_array($models)) $models = [$models]; - foreach ($models as $model) { - $model->push(); - } - } - } - - /** - * Удаление записи. - */ - public function delete() - { - $pk = static::primaryKey(); - if (empty($this->$pk)) { - $this->hook('nothingDelete'); - return $this; - } - $this->hook('beforeDelete'); - $qr = static::getDb(true)->table(static::tableName()) - ->where($pk, $this->$pk)->limit(1)->delete(); - $this->hook('afterDelete', $qr->rowCount()); - if (0 < $qr->rowCount()) { - static::getDb()->identityMapUnset($this, $pk); - $this->$pk = null; - } - return $this; - } - - /** - * Обновление данных модели из базы. - */ - public function reload() - { - $pv = $this->primaryValue(); - if (!$pv) return; - $this->fill((array) static::find($pv)); - } - - /** - * Создание модели записи. - * @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 array|int столбец или столбцы - * @return static|static[] - */ - public static function find($ids) - { - $ids = func_num_args() > 1 ? func_get_args() : $ids; - $db = static::getDb(); - return (new QueryBuilder($db, static::class))->find($ids); - } - - /** - * Поиск по sql-запросу. - * @param string sql-запрос - * @param array|null значения запроса для экранирования - * @return static|static[] - */ - public static function query(string $sql, array $values = null) - { - $qr = static::getDb()->query($sql, $values); - return strstr($qr->stmt()->queryString, 'LIMIT 1') - ? $qr->object(static::class) - : $qr->objectAll(static::class); - } - - /** - * Получение или создание новой записи. - * @param array свойства поиска - * @param array свойства вставки - * @return static - */ - public static function firstOrCreate(array $findProps, array $createProps = null) - { - $keys = array_keys($findProps); - $vals = array_values($findProps); - foreach (static::columns() as $i => $key) { - if (!in_array($key, $keys)) { - unset($keys[$i]); - unset($vals[$i]); - } - } - $record = static::whereRowValues($keys, $vals)->one(); - if ($record) return $record; - return static::create(array_merge($findProps, $createProps)); - } - - /** - * Проброс сборщика запросов через магический вызов статического метода. - * @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::getDb(); - return (new QueryBuilder($db, static::class))->$name(...$args); - } - throw new \BadMethodCallException(sprintf( - 'Call to undefined method %s::%s()', __CLASS__, $name - )); - } - - /** - * Описание связей модели. - * @return array - */ - protected static function relations(): array - { - return []; - } - - /** - * Подгрузка связанных записей. - * @param string имя связи - * @param array|null столбцы - * @return self - */ - public function loadRelated(string $name, array $columns = null, QueryBuilder $query = null) - { - $relation = static::getRelation($name); - if ($query) { - $qb = $query->where($relation->foreignKey, $this->{$relation->localKey}); - } else { - $qb = $relation->foreignModel::where($relation->foreignKey, $this->{$relation->localKey}); - } - if ($relation->type === 'hasOne') $qb->limit(1); - $models = $qb->get($columns); - $this->setRelatedCollection($relation->name, $models); - return $this; - } - - public function load(...$props) - { - foreach ($props as &$prop) { - if (is_string($prop)) { - @list($name, $columns) = explode(':', $prop); - $this->loadRelated($name, $columns); - } else if (is_array($prop)) { - foreach ($prop as $name => &$sub) { - $columns = null; - $query = null; - if (is_string($name)) { - if (is_string($sub)) { - $columns = $sub; - } else if (is_callable($sub)) { - @list($name, $columns) = explode(':', $name); - $relation = $this->getRelation($name); - if (!$relation) continue; - $db = static::getDb(); - $query = new QueryBuilder($db, $relation->foreignModel); - $sub($query); - } - } else if (is_string($sub)) { - @list($name, $columns) = explode(':', $sub); - } else { - throw new \InvalidArgumentException('Incorrect load syntax'); - } - $this->loadRelated($name, $columns, $query); - } - } - } - } - - /** - * Получение данных модели. - * @return array - */ - public function getData(): array - { - return $this->modelData; - } - - /** - * Получение всех связанных записей модели. - * @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; - } - - /** - * Установка свойства. - * @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]); - } - - // Конвертация - - /** - * Конвертация данных и связей модели в массив. - * @return array - */ - public function toArray(): array - { - 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(); - } - - // protected static $list = []; - // protected static $map = []; - - // public static function list(): array - // { - // return static::$list; - // } - - // public static function map(): array - // { - // return static::$map; - // } - - // public static function size(): int - // { - // return count(static::$map); - // } - - // public static function getCached($id = null) - // { - // if (null === $id) { - // return static::list(); - // } else if (is_array($id)) { - // $result = []; - // foreach ($id as $sub) { - // $result[] = static::getCached($sub); - // } - // return $result; - // } else { - // return static::$map[$id]; - // } - // } -} diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php deleted file mode 100644 index 0d352e3..0000000 --- a/src/QueryBuilder.php +++ /dev/null @@ -1,375 +0,0 @@ - - */ -namespace Evas\Orm; - -use Evas\Db\Builders\QueryBuilder as DbQueryBuilder; -use Evas\Db\Interfaces\DatabaseInterface; -// use Evas\Orm\Model; -use Evas\Orm\ActiveRecord; -use Evas\Orm\Relation; - -class QueryBuilder extends DbQueryBuilder -{ - /** @var string разделитель для полей связанных записей */ - public static $relationDataSeparator = '_-_'; - - /** @var string класс модели данных */ - protected $model; - - /** @var array обработанный результат запроса в виде моделей */ - protected $result = []; - - - /** - * Расширяем конструктор передачей класса модели. - * @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 - */ - public function fromModel(string $model) - { - $this->model = $model; - $this->from($model::tableName()); - return $this; - } - - /** - * Преобразование полученых записей. - * @param array|null записи в виде массива - * @return array записи в виде моделей - */ - private function prepareGetResult(array $rows): array - { - foreach ($rows as $row) { - $this->prepareOneResult($row); - } - return array_values($this->result); - } - - /** - * Преобразование полученой записи. - * @param array|null запись в виде массива - * @return ActiveRecord запись в виде модели - */ - private function prepareOneResult(array $row = null) - { - if (!$row) return $row; - $foreigns = []; - if (count($this->withs) > 0) { - foreach ($row as $key => $value) { - @[$fname, $fkey] = explode(static::$relationDataSeparator, $key, 2); - if ($fkey && in_array($fname, array_keys($this->withs))) { - $foreigns[$fname][$fkey] = $value; - unset($row[$key]); - } - } - } - $id = $row[$this->model::primaryKey()]; - if (!isset($this->result[$id])) { - $this->result[$id] = new $this->model($row); - } - $result = $this->result[$id]; - foreach ($foreigns as $fname => $subs) { - $this->withs[$fname][0]->addRelated($result, $subs); - } - return $result; - } - - /** - * Выполнение select-запроса с получением результирующих строк в виде массива моделей. - * @param array|null столбцы - * @return array модели - */ - public function get($columns = null): ?array - { - $this->result = []; - $this->applyWiths(); - $rows = parent::get($columns); - return ( - count($this->aggregates) < 1 && count($this->groups) < 1 - && count($this->withs) >= count($this->joins) - ) - ? $this->prepareGetResult($rows) - : $rows; - } - - /** - * Выполнение select-запроса с получением результирующей строки в виде модели. - * @param array|null столбцы - * @return ActiveRecord|null - */ - public function one($columns = null): ?ActiveRecord - { - $this->result = []; - if (count($this->withs)) { - $rows = $this->limit(1)->get($columns); - return count($rows) ? $rows[0] : null; - } - $row = parent::one($columns); - return $this->prepareOneResult($row); - } - - protected function applyWiths() - { - if (count($this->withs)) { - if ($this->limit) { - // $this->fromSub(function ($query) { - // $query->from($this->from)->limit($this->limit); - // }, $this->from); - // $this->fromSub($this->getSql(), $this->from); - $this->fromSub($this, $this->from); - $this->bindings['where'] = []; - $this->wheres = []; - $this->limit(null); - } - $columns = static::prepareModelColumns($this->columns, $this->model); - $this->addSelect($columns); - foreach ($this->withs as [$relation, $columns, $query]) { - $this->applyWith($relation, $columns, $query); - } - } - } - - protected function applyWith(Relation $relation, array $columns = null, self $query = null) - { - $columns = static::prepareModelColumns($columns, $relation->foreignModel, $relation->name); - $this->addSelect($columns); - $this->leftJoinSub($query ?? $relation->foreignTable, $relation->name, $relation->foreignFullKey, $relation->localFullKey); - return $this; - } - - protected $withs = []; - - protected function addWith(string $name, array $columns = null, self $query = null) - { - $relation = $this->getRelation($name); - if ($relation) { - if (!isset($this->withs[$relation->name])) { - $this->withs[$relation->name] = [$relation, $columns, $query]; - } - } - return $this; - } - - public function with(...$props) - { - foreach ($props as &$prop) { - if (is_string($prop)) { - @list($name, $columns) = explode(':', $prop); - $this->addWith($name, $columns); - - } else if (is_array($prop)) { - foreach ($prop as $name => &$sub) { - $columns = null; - $query = null; - if (is_string($name)) { - if (is_string($sub)) { - $columns = $sub; - } else if (is_callable($sub)) { - @list($name, $columns) = explode(':', $name); - $relation = $this->getRelation($name); - if (!$relation) continue; - $query = new static($this->db, $relation->foreignModel); - $sub($query); - } - } else if (is_string($sub)) { - @list($name, $columns) = explode(':', $sub); - } else { - throw new \InvalidArgumentException('Incorrect with syntax'); - } - $this->addWith($name, $columns, $query); - } - } - } - return $this; - } - - protected function addHas( - bool $isNot, bool $isWith, string $relationName, $operator = null, $value = null - ) { - @[$relationName, $columns] = explode(':', $relationName); - $relation = $this->getRelation($relationName); - if ($relation) { - $selfColumns = static::prepareModelColumns($this->columns, $this->model); - $this->select($selfColumns); - if ($isNot) { - $this->leftOuterJoinSub($relation->foreignTable, $relation->name, $relation->foreignFullKey, $relation->localFullKey); - $this->whereNull($relation->foreignFullKey); - } else { - if (func_num_args() > 3) { - if ($this->isQueryable($operator)) { - if ($operator instanceof \Closure) { - $cb = $operator; - $cb($operator = $this->forSubQuery()); - } - foreach ($operator->wheres as $where) { - if (isset($where['columns'])) foreach($where['columns'] as &$column) { - $parts = explode('.', $column); - if (count($parts) < 2 || $parts[0] != $relationName) { - array_unshift($parts, $relation); - $column = implode('.', $parts); - } - } - $this->pushWhere($where['type'], $where); - } - $this->addBindings('where', $operator->getBindings('where')); - } else { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 4 - ); - } - if ($columns) { - $columns = explode(',', $columns); - foreach ($columns as $column) { - $column = $relation->name . '.' . $column; - // $this->count($column); - // $this->havingAggregate('count', $column, $operator, $value); - $this->where($column, $operator, $value); - // $this->whereNotNull($column); - // $this->where($column, '!=', 0); - } - } else { - $foreignFullPrimary = $relation->name . '.' . $relation->foreignModel::primaryKey(); - $this->count($foreignFullPrimary); - $this->havingAggregate('count', $foreignFullPrimary, $operator, $value); - } - } else if ($columns) { - $columns = explode(',', $columns); - foreach ($columns as $column) { - $column = $relation->name . '.' . $column; - // $this->count($column); - // $this->havingAggregate('count', $column, $operator, $value); - // $this->where($column, $operator, $value); - $this->whereNotNull($column); - // $this->where($column, '!=', 0); - } - // $columns = static::prepareModelColumns($columns, $relation->foreignModel, $relation->name); - } - $this->joinSub($relation->foreignTable, $relation->name, $relation->foreignFullKey, $relation->localFullKey); - $this->groupBy($relation->localFullKey); - } - } - return $this; - } - - public function has(string $relationName, $operator = null, $value = null) - { - return $this->addHas(false, false, ...func_get_args()); - } - - public function notHas(string $relationName, $operator = null, $value = null) - { - return $this->addHas(true, false, ...func_get_args()); - } - - public function withHas(string $relationName, $operator = null, $value = null) - { - return $this->addHas(false, true, ...func_get_args()); - } - - - protected function getRelation(string $name): ?Relation - { - return $this->model::getRelation($name); - } - - // protected function parseTableWithColumns(string $value) - // { - // @list($table, $columns) = explode(':', $value); - // return [$table, $columns]; - // } - - protected static function prepareModelColumns($columns, string $model, string $asPrefix = null) - { - $columns = static::prepareColumns($columns); - if (!$columns) $columns = $model::columns(); - $table = $model::tableName(); - $keys = []; - foreach ($columns as &$column) { - if (!empty($asPrefix)) { - $as = $asPrefix . static::$relationDataSeparator . $column; - $col = "{$asPrefix}.{$column}"; - } else { - $as = $column; - $col = "{$table}.{$column}"; - } - $keys[$as] = $col; - } - return $keys; - } - - protected static function prepareColumns($columns): ?array - { - return (empty($columns) || $columns = '*' || (is_array($columns) && in_array('*', $columns))) - ? null - : explode(',', str_replace(' ', '', $columns)); - } - - // public function with(...$props) - // { - // // $relations = []; - // foreach ($props as &$prop) { - // if (is_string($prop)) { - // @list($name, $columns) = explode(':', $prop); - // $columns = $this->prepareColumns($columns) - // $relation = $this->model->$name(); - // // $this->base->leftJoin($relation->name) - // // $sql = 'SELECT * FROM `user`' - // // . ' LEFT JOIN `group` ON `group`.`user_id` = `user`.`id`'; - // // $relation = $this->getRelation($name); - // } else if (is_array($prop)) { - // foreach ($prop as $name => &$sub) { - // $columns = null; - // if (is_string($name)) { - // if (is_string($sub)) { - // $columns = $sub; - // } else if (is_callable($sub)) { - // @list($name, $columns) = $name; - // $relation = $this->model->$name(); - // $sub(new static($relation)); - // } - // } else if (is_string($sub)) { - // @list($name, $columns) = explode(':', $sub); - // } else { - // throw new \Exception('Incorrect!'); - // } - // $columns = $this->prepareColumns($columns); - // $relation = $this->model->$name(); - // // $relation = $this->getRelation($name); - // } - // } - // } - // } - - public function withCount(...$props) - {} - - public function withSum(...$props) - {} - - public function withMax(...$props) - {} - - public function withMin(...$props) - {} - - public function withAvg(...$props) - {} - - public function withExists(...$props) - {} -} diff --git a/src/RelatedCollection.php b/src/RelatedCollection.php deleted file mode 100644 index 10e89fb..0000000 --- a/src/RelatedCollection.php +++ /dev/null @@ -1,93 +0,0 @@ - - */ -namespace Evas\Orm; - -use Evas\Base\Help\Collection; -use Evas\Base\Help\PhpHelp; -use Evas\Orm\ActiveRecord; -use Evas\Orm\Relation; - -class RelatedCollection extends Collection -{ - /** @var ActiveRecord модель */ - protected $model; - /** @var string связь */ - 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)) { - $item = new $this->relation->foreignModel($item); - } - if (!($id = $item->primaryValue()) || !$this->has($id)) { - $this->ids[] = $id; - return parent::add($item); - } - return $this; - } - - /** - * Конструктор. - * @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 deleted file mode 100644 index f0b91be..0000000 --- a/src/Relation.php +++ /dev/null @@ -1,92 +0,0 @@ - - */ -namespace Evas\Orm; - -use Evas\Orm\ActiveRecord; - -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); - if (!$localKey) $localKey = $localModel::primaryKey(); - $this->type = $type; - $this->localModel = $localModel; - $this->localKey = $localKey; - $this->foreignModel = $foreignModel; - $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 (strlen($pk) > 1 && strrpos($pk, 's') == strlen($pk) - 1) { - $pk = substr($pk, 0, strlen($pk) - 1); - } - $pk .= '_id'; - } - return $pk; - } - - /** - * Установка имени связей модели. - * @param string имя - * @return self - */ - public function setName(string $name) - { - $this->name = $name; - $this->foreignFullKey = "$this->name.$this->foreignKey"; - return $this; - } - - /** - * Добавление связи с моделью. - * @param ActiveRecord модель - * @param array данные внешней модели - * @return ActiveRecord модель - */ - public function addRelated(ActiveRecord $model, array $foreignData) - { - return $model->addRelated($this->name, new $this->foreignModel($foreignData)); - } -} diff --git a/src/Traits/RelationsTrait.php b/src/Traits/RelationsTrait.php deleted file mode 100644 index 69e6d7f..0000000 --- a/src/Traits/RelationsTrait.php +++ /dev/null @@ -1,131 +0,0 @@ - - */ -namespace Evas\Orm\Traits; - -use Evas\Base\Help\PhpHelp; -use Evas\Orm\ActiveRecord; -use Evas\Orm\OrmException; -use Evas\Orm\RelatedCollection; -use Evas\Orm\Relation; - -trait RelationsTrait -{ - /** @static array связи */ - protected static $relations; - /** @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); - if ($relation->type === 'hasOne') { - 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; - } - - /** - * Инициализация связей. - * @throws \InvalidArgumentException - */ - protected static function initRelations() - { - $relations = static::relations(); - if ($relations) foreach ($relations as $name => &$relation) { - if (!$relation instanceof Relation) { - throw new \InvalidArgumentException(sprintf( - 'Relation must be instance of %s, %s given', - Relation::class, PhpHelp::getType($relation) - )); - } - $relation->setName($name); - } - static::$relations = &$relations; - } - - /** - * Получение связи по имени. - * @param string имя связи - * @return Relation|null - */ - public static function getRelation(string $name): ?Relation - { - if (is_null(static::$relations)) { - static::initRelations(); - } - return static::$relations[$name] ?? null; - } - - /** - * Проверка наличия связи. - * @param string имя связи - * @return bool - */ - public static function hasRelation(string $name): bool - { - if (is_null(static::$relations)) { - static::initRelations(); - } - return isset(static::$relations[$name]); - } - - /** - * Установка множественной связи. - * @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); - } -} From 4689c6b86853491044eac37673437998527ae61c Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:16 +0300 Subject: [PATCH 11/60] Create ActiveRecordDatabaseTrait.php --- src/Traits/ActiveRecordDatabaseTrait.php | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/Traits/ActiveRecordDatabaseTrait.php diff --git a/src/Traits/ActiveRecordDatabaseTrait.php b/src/Traits/ActiveRecordDatabaseTrait.php new file mode 100644 index 0000000..22b561a --- /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 getDb(bool $write = false): DatabaseInterface + { + $dbname = static::getDbName($write); + return App::db($dbname); + } + + /** + * Получение имени соединения с базой данных. + * @param bool использовать ли соединение для записи + * @return string|null + */ + public static function getDbName(bool $write = false): ?string + { + return true === $write && !empty(static::$dbnameWrite) + ? static::$dbnameWrite : static::$dbname; + } +} From 13ed4b0a4c4a6067cb5b36191d00f611548b3e29 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:23 +0300 Subject: [PATCH 12/60] Create ActiveRecordTableTrait.php --- src/Traits/ActiveRecordTableTrait.php | 80 +++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/Traits/ActiveRecordTableTrait.php diff --git a/src/Traits/ActiveRecordTableTrait.php b/src/Traits/ActiveRecordTableTrait.php new file mode 100644 index 0000000..602697b --- /dev/null +++ b/src/Traits/ActiveRecordTableTrait.php @@ -0,0 +1,80 @@ + + */ +namespace Evas\Orm\Traits; + +use Evas\Db\Interfaces\TableInterface; + +trait ActiveRecordTableTrait +{ + /** @var string имя таблицы */ + public static $tableName; + + /** + * Генерация имени таблицы из имени класса. + * @return string + */ + public static function generateTableName(): string + { + $className = static::class; + $lastSlash = strrpos($className, '\\'); + if ($lastSlash > 0) { + $className = substr($className, $lastSlash + 1); + } + return strtolower(preg_replace('/([a-z0-9]+)([A-Z]{1})/', '$1_$2', $className)) . 's'; + } + + /** + * Получение имени таблицы. + * @return string + */ + public static function tableName(): string + { + if (empty(static::$tableName)) { + static::$tableName = static::generateTableName(); + } + return static::$tableName; + } + + /** + * Получение объекта таблицы. + * @param bool использовать ли соединение с БД для записи + * @return TableInterface + */ + public static function table(bool $write = false): TableInterface + { + return static::getDb($write)->table(static::tableName()); + } + + /** + * Получение первичного ключа. + * @param bool|null переполучить из схемы заново + * @return string + */ + public static function primaryKey(bool $reload = false): string + { + return static::table()->primaryKey($reload); + } + + /** + * Получение столбцов таблицы. + * @param bool|null переполучить из схемы заново + * @return array + */ + public static function columns(bool $reload = false): array + { + return static::table()->columns($reload); + } + + /** + * Получение id последней записи. + * @return int|null + */ + public static function lastInsertId(): ?int + { + return static::table()->lastInsertId(); + } +} From 2eda8bea2f6faa7d9696ae9df0c39fe02a90d0a2 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:30 +0300 Subject: [PATCH 13/60] Create ActiveRecordDataTrait.php --- src/Traits/ActiveRecordDataTrait.php | 96 ++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/Traits/ActiveRecordDataTrait.php diff --git a/src/Traits/ActiveRecordDataTrait.php b/src/Traits/ActiveRecordDataTrait.php new file mode 100644 index 0000000..d7a34d6 --- /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]); + } +} From 99c178e8710cbe81a285a1e2156926ec4a957877 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:32 +0300 Subject: [PATCH 14/60] Create ActiveRecordConvertTrait.php --- src/Traits/ActiveRecordConvertTrait.php | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/Traits/ActiveRecordConvertTrait.php diff --git a/src/Traits/ActiveRecordConvertTrait.php b/src/Traits/ActiveRecordConvertTrait.php new file mode 100644 index 0000000..cfa6471 --- /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(); + } +} From c188a82fd83edeb737d3eb18f7bbc32aed4bf33a Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:36 +0300 Subject: [PATCH 15/60] Create ActiveRecordQueryTrait.php --- src/Traits/ActiveRecordQueryTrait.php | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/Traits/ActiveRecordQueryTrait.php diff --git a/src/Traits/ActiveRecordQueryTrait.php b/src/Traits/ActiveRecordQueryTrait.php new file mode 100644 index 0000000..3d72320 --- /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::getDb()->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::getDb(); + 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::getDb(); + return (new QueryBuilder($db, static::class))->find($ids); + } +} From 882520157ab9eac28be7eed82548c27079643ad7 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:40 +0300 Subject: [PATCH 16/60] Create ActiveRecordStateTrait.php --- src/Traits/ActiveRecordStateTrait.php | 73 +++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/Traits/ActiveRecordStateTrait.php diff --git a/src/Traits/ActiveRecordStateTrait.php b/src/Traits/ActiveRecordStateTrait.php new file mode 100644 index 0000000..550ac06 --- /dev/null +++ b/src/Traits/ActiveRecordStateTrait.php @@ -0,0 +1,73 @@ + + */ +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 ?? []); + return array_merge( + array_fill_keys(array_keys(array_diff($state ?? [], $props)), null), + array_diff_assoc($props, $state ?? []) + ); + } + } +} From 19198ee874d3566c02b64919dacb3cadeb6af21d Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:43 +0300 Subject: [PATCH 17/60] Create ActiveRecord.php --- src/ActiveRecord.php | 126 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/ActiveRecord.php diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php new file mode 100644 index 0000000..eef00af --- /dev/null +++ b/src/ActiveRecord.php @@ -0,0 +1,126 @@ + + */ +namespace Evas\Orm; + +use Evas\Base\Help\HooksTrait; +use Evas\Orm\Exceptions\LastInsertIdUndefinedException; +use Evas\Orm\Traits\ActiveRecordConvertTrait; +use Evas\Orm\Traits\ActiveRecordDatabaseTrait; +use Evas\Orm\Traits\ActiveRecordDataTrait; +use Evas\Orm\Traits\ActiveRecordStateTrait; +use Evas\Orm\Traits\ActiveRecordTableTrait; +use Evas\Orm\Traits\ActiveRecordQueryTrait; + +class ActiveRecord implements \JsonSerializable +{ + // подключаем конвертацию + use ActiveRecordConvertTrait; + + use ActiveRecordDatabaseTrait; + + use ActiveRecordDataTrait; + + use ActiveRecordStateTrait; + + use ActiveRecordTableTrait; + + use ActiveRecordQueryTrait; + + // подключаем поддержку произвольных хуков в наследуемых классах + use HooksTrait; + + /** + * Конструктор. + * @param array|null свойства модели + */ + public function __construct(array $props = null) + { + $this->hook('beforeConstruct', $props); + $creating = empty($props[static::primaryKey()]); + + $this->hook($creating ? 'beforeCreate' : 'beforeGet', $props); + if (!empty($props)) $this->fill($props); + + $this->hook($creating ? 'afterCreate' : 'afterGet'); + $this->hook('afterConstruct'); + } + + /** + * Сохранение записи. + * @return self + * @throws LastInsertIdUndefinedException + */ + public function save() + { + $props = $this->getUpdatedProps(); + if (empty($props)) { + $this->hook('nothingSave'); + return $this; + } + + $this->hook('beforeSave'); + $pk = static::primaryKey(); + if ($this->isStateHasPrimaryValue()) { + $this->hook('beforeUpdate'); + static::table(true)->where($pk, $this->$pk)->update($props); + $this->hook('afterUpdate'); + } else { + $this->hook('beforeInsert'); + static::table(true)->insert($props); + $this->$pk = static::lastInsertId(); + if (empty($this->$pk)) { + throw new LastInsertIdUndefinedException(); + } + $this->hook('afterInsert'); + } + $this->hook('afterSave'); + // 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(); + } + } + + /** + * Удаление записи. + */ + public function delete() + { + $pk = static::primaryKey(); + if (empty($this->$pk)) { + $this->hook('nothingDelete'); + return $this; + } + $this->hook('beforeDelete'); + $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->$pk = null; + } + return $this; + } + + /** + * Обновление данных модели из базы. + */ + public function reload() + { + $pv = $this->primaryValue(); + if (!$pv) return; + $this->fill((array) static::find($pv)); + } +} From 37316da496b9b8abb63f943ddefc75824102e037 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:47 +0300 Subject: [PATCH 18/60] Create IdentityMap.php --- src/IdentityMap.php | 56 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/IdentityMap.php diff --git a/src/IdentityMap.php b/src/IdentityMap.php new file mode 100644 index 0000000..29a4d49 --- /dev/null +++ b/src/IdentityMap.php @@ -0,0 +1,56 @@ +unsetAll(); + } + + public function has($model) + { + return $this->models->offsetExists($model); + } + + public function set($model) + { + $state = $model->toArray(); + $this->models->offsetSet($model, $state); + return $this; + } + + public function get($model) + { + return $this->models->offsetGet($model); + } + + public function unset($model) + { + $this->models->offsetUnset($model); + return $this; + } + + public function unsetAll() + { + $this->models = new \WeakMap; + return $this; + } + + public function __toString() + { + return json_encode(["models" => $this->models]); + } +} From 1d9a354dd6352126ba4e020396ccef81c5211db4 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 03:36:49 +0300 Subject: [PATCH 19/60] Create ModelIdentity.php --- src/Identity/ModelIdentity.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/Identity/ModelIdentity.php diff --git a/src/Identity/ModelIdentity.php b/src/Identity/ModelIdentity.php new file mode 100644 index 0000000..4203171 --- /dev/null +++ b/src/Identity/ModelIdentity.php @@ -0,0 +1,21 @@ +dbname = $dbname; + $this->class = $class; + $this->id = $id; + } + + public function __toString() + { + return implode(':', [$this->dbname, $this->class, $this->id]); + } +} From 06a955f9daa61d767cff9f03558463dd02f73331 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 05:39:05 +0300 Subject: [PATCH 20/60] Fix phpdoc --- src/ActiveRecord.php | 2 +- src/Traits/ActiveRecordConvertTrait.php | 2 +- src/Traits/ActiveRecordDataTrait.php | 2 +- src/Traits/ActiveRecordDatabaseTrait.php | 2 +- src/Traits/ActiveRecordQueryTrait.php | 2 +- src/Traits/ActiveRecordTableTrait.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index eef00af..54d9ac1 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -1,6 +1,6 @@ */ diff --git a/src/Traits/ActiveRecordConvertTrait.php b/src/Traits/ActiveRecordConvertTrait.php index cfa6471..4c563f9 100644 --- a/src/Traits/ActiveRecordConvertTrait.php +++ b/src/Traits/ActiveRecordConvertTrait.php @@ -1,6 +1,6 @@ */ diff --git a/src/Traits/ActiveRecordDataTrait.php b/src/Traits/ActiveRecordDataTrait.php index d7a34d6..6ccebe5 100644 --- a/src/Traits/ActiveRecordDataTrait.php +++ b/src/Traits/ActiveRecordDataTrait.php @@ -1,6 +1,6 @@ */ diff --git a/src/Traits/ActiveRecordDatabaseTrait.php b/src/Traits/ActiveRecordDatabaseTrait.php index 22b561a..7321913 100644 --- a/src/Traits/ActiveRecordDatabaseTrait.php +++ b/src/Traits/ActiveRecordDatabaseTrait.php @@ -1,6 +1,6 @@ */ diff --git a/src/Traits/ActiveRecordQueryTrait.php b/src/Traits/ActiveRecordQueryTrait.php index 3d72320..536162e 100644 --- a/src/Traits/ActiveRecordQueryTrait.php +++ b/src/Traits/ActiveRecordQueryTrait.php @@ -1,6 +1,6 @@ */ diff --git a/src/Traits/ActiveRecordTableTrait.php b/src/Traits/ActiveRecordTableTrait.php index 602697b..c9c55d2 100644 --- a/src/Traits/ActiveRecordTableTrait.php +++ b/src/Traits/ActiveRecordTableTrait.php @@ -1,6 +1,6 @@ */ From fc9db21a2309671490bef7eb96ca0c2a4b2971ec Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 08:05:22 +0300 Subject: [PATCH 21/60] Create QueryBuilder.php --- src/QueryBuilder.php | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/QueryBuilder.php diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php new file mode 100644 index 0000000..a8be15b --- /dev/null +++ b/src/QueryBuilder.php @@ -0,0 +1,78 @@ + + */ +namespace Evas\Orm; + +use Evas\Db\Builders\QueryBuilder as DbQueryBuilder; +use Evas\Db\Interfaces\DatabaseInterface; + +class QueryBuilder extends DbQueryBuilder +{ + /** @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 + { + if ($columns) $this->addSelect(...func_get_args()); + return $this->query()->objectAll($this->model); + } + + /** + * Выполнение select-запроса с получением одной записи. + * @param array|null столбцы для получения + * @return array|null найденная запись + */ + public function one($columns = null) + { + if ($columns) $this->addSelect(...func_get_args()); + return $this->limit(1)->query()->object($this->model); + } +} From 76197f14c101f0f50ab06a7268158c96cf3bed43 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Fri, 4 Nov 2022 08:05:34 +0300 Subject: [PATCH 22/60] Update ActiveRecord.php --- src/ActiveRecord.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 54d9ac1..e23d81b 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -39,8 +39,9 @@ class ActiveRecord implements \JsonSerializable */ public function __construct(array $props = null) { + $creating = empty($this->primaryValue()); + if (!$creating) $this->saveState(); $this->hook('beforeConstruct', $props); - $creating = empty($props[static::primaryKey()]); $this->hook($creating ? 'beforeCreate' : 'beforeGet', $props); if (!empty($props)) $this->fill($props); @@ -62,22 +63,22 @@ public function save() return $this; } - $this->hook('beforeSave'); + $this->hook('beforeSave', $props); $pk = static::primaryKey(); if ($this->isStateHasPrimaryValue()) { - $this->hook('beforeUpdate'); + $this->hook('beforeUpdate', $props); static::table(true)->where($pk, $this->$pk)->update($props); - $this->hook('afterUpdate'); + $this->hook('afterUpdate', $props); } else { - $this->hook('beforeInsert'); + $this->hook('beforeInsert', $props); static::table(true)->insert($props); $this->$pk = static::lastInsertId(); if (empty($this->$pk)) { throw new LastInsertIdUndefinedException(); } - $this->hook('afterInsert'); + $this->hook('afterInsert', $props); } - $this->hook('afterSave'); + $this->hook('afterSave', $props); // return static::getDb()->identityMapUpdate($this, $pk); return $this->saveState(); } From 2de21a70ec4558058fb22b573b62a8bce9e6539c Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 7 Nov 2022 17:26:54 +0300 Subject: [PATCH 23/60] Weak map not help with IdentityMap :anguished: --- src/ActiveRecord.php | 5 ++ src/Identity/ModelIdentity.php | 16 +++++-- src/IdentityMap.php | 61 +++++++++++++++++------- src/QueryBuilder.php | 9 +++- src/Traits/ActiveRecordDataTrait.php | 8 ++-- src/Traits/ActiveRecordIdentityTrait.php | 38 +++++++++++++++ 6 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 src/Traits/ActiveRecordIdentityTrait.php diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index e23d81b..7939497 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -15,6 +15,9 @@ use Evas\Orm\Traits\ActiveRecordTableTrait; use Evas\Orm\Traits\ActiveRecordQueryTrait; +use Evas\Orm\Traits\ActiveRecordIdentityTrait; + + class ActiveRecord implements \JsonSerializable { // подключаем конвертацию @@ -33,6 +36,8 @@ class ActiveRecord implements \JsonSerializable // подключаем поддержку произвольных хуков в наследуемых классах use HooksTrait; + use ActiveRecordIdentityTrait; + /** * Конструктор. * @param array|null свойства модели diff --git a/src/Identity/ModelIdentity.php b/src/Identity/ModelIdentity.php index 4203171..e39fbe5 100644 --- a/src/Identity/ModelIdentity.php +++ b/src/Identity/ModelIdentity.php @@ -3,12 +3,11 @@ class ModelIdentity { - public readonly ?string $dbname = null; - public readonly string $class; - public readonly $id; + protected ?string $dbname; + protected string $class; + protected $id; - public function __construct(string $class, $id, ?string $dbname = null) - { + public function __construct(string $class, $id, ?string $dbname = null) { $this->dbname = $dbname; $this->class = $class; $this->id = $id; @@ -18,4 +17,11 @@ public function __toString() { return implode(':', [$this->dbname, $this->class, $this->id]); } + + public static function createFromModel(object $model) + { + return ($id = $model->primaryValue()) + ? new static(get_class($model), $id, $model::getDbName()) + : null; + } } diff --git a/src/IdentityMap.php b/src/IdentityMap.php index 29a4d49..b8bbacc 100644 --- a/src/IdentityMap.php +++ b/src/IdentityMap.php @@ -17,40 +17,69 @@ public static function instance() protected function __construct() { - $this->unsetAll(); + $this->resetModels(); } - public function has($model) + public function resetModels() { - return $this->models->offsetExists($model); + $this->models = new \WeakMap; + return $this; } - public function set($model) + public static function count(): int { - $state = $model->toArray(); - $this->models->offsetSet($model, $state); - return $this; + return static::models()->count(); } - public function get($model) + public static function models() { - return $this->models->offsetGet($model); + return static::instance()->models; } - public function unset($model) + public static function has($identity) { - $this->models->offsetUnset($model); - return $this; + return static::models()->offsetExists($identity); } - public function unsetAll() + public static function set($identity, $model = null) { - $this->models = new \WeakMap; - return $this; + if (func_num_args() < 2) { + [$model, $identity] = [$identity, $identity->identity()]; + } + // $state = $model->toArray(); + static::models()->offsetSet($identity, $model); + return static::instance(); + } + + public static function get($identity) + { + return static::models()->offsetGet($identity); + } + + public static function getOrSet($identity, $model = null) + { + if (func_num_args() < 2) { + [$model, $identity] = [$identity, $identity->identity()]; + } + if (!static::has($identity)) { + static::set($identity, $model); + } + return static::get($identity); + } + + public static function unset($identity) + { + static::models()->offsetUnset($identity); + return static::instance(); + } + + public static function unsetAll() + { + static::instance()->resetModels(); } public function __toString() { - return json_encode(["models" => $this->models]); + return json_encode(["models_count" => static::count()]); } } diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index a8be15b..323bb59 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -62,7 +62,11 @@ protected function primaryKey(): string public function get($columns = null): array { if ($columns) $this->addSelect(...func_get_args()); - return $this->query()->objectAll($this->model); + $result = $this->query()->objectAll($this->model); + foreach ($result as &$model) { + $model = $model->identityMapSave(); + } + return $result; } /** @@ -73,6 +77,7 @@ public function get($columns = null): array public function one($columns = null) { if ($columns) $this->addSelect(...func_get_args()); - return $this->limit(1)->query()->object($this->model); + $model = $this->limit(1)->query()->object($this->model); + return $model->identityMapSave(); } } diff --git a/src/Traits/ActiveRecordDataTrait.php b/src/Traits/ActiveRecordDataTrait.php index 6ccebe5..4124a13 100644 --- a/src/Traits/ActiveRecordDataTrait.php +++ b/src/Traits/ActiveRecordDataTrait.php @@ -68,11 +68,11 @@ public function __set(string $name, $value) */ public function __get(string $name) { - if (static::hasRelation($name)) { - return $this->getRelatedCollection($name); - } else { + // if (static::hasRelation($name)) { + // return $this->getRelatedCollection($name); + // } else { return $this->modelData[$name] ?? null; - } + // } } /** diff --git a/src/Traits/ActiveRecordIdentityTrait.php b/src/Traits/ActiveRecordIdentityTrait.php new file mode 100644 index 0000000..9cae1c1 --- /dev/null +++ b/src/Traits/ActiveRecordIdentityTrait.php @@ -0,0 +1,38 @@ + + */ +namespace Evas\Orm\Traits; + +use Evas\Orm\Identity\ModelIdentity; +use Evas\Orm\IdentityMap; + +trait ActiveRecordIdentityTrait +{ + protected $identity; + + public function identity() + { + if (!$this->identity) { + $this->identity = ModelIdentity::createFromModel($this); + } + return $this->identity; + } + + public function identityMapSave() + { + // echo '
';
+        echo dumpOrm($this->identity());
+        echo dumpOrm(spl_object_id($this->identity()));
+        echo dumpOrm(IdentityMap::has($this->identity()));
+        // echo '
'; + return IdentityMap::getOrSet($this); + } + + public function identityMapRemove() + { + IdentityMap::unset($this->identity()); + } +} From 7643694954da16ab614b1200943ba8acd8d3b1d0 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 7 Nov 2022 20:03:47 +0300 Subject: [PATCH 24/60] Update ActiveRecord.php --- src/ActiveRecord.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 7939497..12203dc 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -125,8 +125,9 @@ public function delete() */ public function reload() { - $pv = $this->primaryValue(); - if (!$pv) return; - $this->fill((array) static::find($pv)); + return ($pv = $this->primaryValue()) + ? static::find($pv) + // ? $this->fill((array) static::find($pv)) + : $this; } } From 769a2667dd006f7c97041d6c9424493ac9ffa4b1 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 7 Nov 2022 20:04:21 +0300 Subject: [PATCH 25/60] Update ActiveRecordIdentityTrait.php --- src/Traits/ActiveRecordIdentityTrait.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Traits/ActiveRecordIdentityTrait.php b/src/Traits/ActiveRecordIdentityTrait.php index 9cae1c1..9d8eab2 100644 --- a/src/Traits/ActiveRecordIdentityTrait.php +++ b/src/Traits/ActiveRecordIdentityTrait.php @@ -23,11 +23,6 @@ public function identity() public function identityMapSave() { - // echo '
';
-        echo dumpOrm($this->identity());
-        echo dumpOrm(spl_object_id($this->identity()));
-        echo dumpOrm(IdentityMap::has($this->identity()));
-        // echo '
'; return IdentityMap::getOrSet($this); } From a16c7a80ec4538f8c6001326825ccdbd030ca980 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 7 Nov 2022 20:04:30 +0300 Subject: [PATCH 26/60] Update IdentityMap.php --- src/IdentityMap.php | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/IdentityMap.php b/src/IdentityMap.php index b8bbacc..748a868 100644 --- a/src/IdentityMap.php +++ b/src/IdentityMap.php @@ -6,7 +6,7 @@ class IdentityMap { protected static $instance; - protected $models; + protected $models = []; public static function instance() @@ -22,23 +22,18 @@ protected function __construct() public function resetModels() { - $this->models = new \WeakMap; + $this->models = []; return $this; } public static function count(): int { - return static::models()->count(); - } - - public static function models() - { - return static::instance()->models; + return count(static::instance()->models); } public static function has($identity) { - return static::models()->offsetExists($identity); + return isset(static::instance()->models[(string) $identity]); } public static function set($identity, $model = null) @@ -46,14 +41,13 @@ public static function set($identity, $model = null) if (func_num_args() < 2) { [$model, $identity] = [$identity, $identity->identity()]; } - // $state = $model->toArray(); - static::models()->offsetSet($identity, $model); + static::instance()->models[(string) $identity] = $model; return static::instance(); } public static function get($identity) { - return static::models()->offsetGet($identity); + return static::instance()->models[(string) $identity] ?? null; } public static function getOrSet($identity, $model = null) @@ -63,13 +57,26 @@ public static function getOrSet($identity, $model = null) } if (!static::has($identity)) { static::set($identity, $model); + return $model; + } else { + $old = static::get($identity); + /** @todo Sync state */ + $props = $old->getUpdatedProps(); + $old->fill($model->getData()); + $old->saveState(); + var_dump($props); + foreach ($props as $name => $value) { + $old->$name = $value; + } + $old->saveState(); + return $old; } - return static::get($identity); + // return static::get($identity); } public static function unset($identity) { - static::models()->offsetUnset($identity); + unset(static::instance()->models[(string) $identity]); return static::instance(); } From b4653f4c5c419c3ed39e97a43d4e11d2048d146a Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 01:49:40 +0300 Subject: [PATCH 27/60] Update QueryBuilder.php --- src/QueryBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 323bb59..8126842 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -78,6 +78,6 @@ public function one($columns = null) { if ($columns) $this->addSelect(...func_get_args()); $model = $this->limit(1)->query()->object($this->model); - return $model->identityMapSave(); + return is_null($model) ? $model : $model->identityMapSave(); } } From 5d2fe05f919f666036d0c40a8740ddb9ad7c2e3b Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 01:49:44 +0300 Subject: [PATCH 28/60] Update IdentityMap.php --- src/IdentityMap.php | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/IdentityMap.php b/src/IdentityMap.php index 748a868..5926dcb 100644 --- a/src/IdentityMap.php +++ b/src/IdentityMap.php @@ -2,6 +2,7 @@ namespace Evas\Orm; use Evas\Orm\Identity\ModelIdentity; +use Evas\Orm\ActiveRecord; class IdentityMap { @@ -15,6 +16,18 @@ public static function instance() return static::$instance; } + protected static function getIdentity($identity, $model = null) + { + if (func_num_args() > 1 && $model instanceof ActiveRecord) { + return [$identity, $model]; + } + if ($identity instanceof ActiveRecord) { + return [$identity->identity(), $identity]; + } else { + return [$identity]; + } + } + protected function __construct() { $this->resetModels(); @@ -33,49 +46,43 @@ public static function count(): int public static function has($identity) { + @[$identity] = static::getIdentity($identity); return isset(static::instance()->models[(string) $identity]); } public static function set($identity, $model = null) { - if (func_num_args() < 2) { - [$model, $identity] = [$identity, $identity->identity()]; - } + @[$identity, $model] = static::getIdentity($identity, $model); static::instance()->models[(string) $identity] = $model; return static::instance(); } public static function get($identity) { + @[$identity] = static::getIdentity($identity); return static::instance()->models[(string) $identity] ?? null; } public static function getOrSet($identity, $model = null) { - if (func_num_args() < 2) { - [$model, $identity] = [$identity, $identity->identity()]; - } + @[$identity, $model] = static::getIdentity($identity, $model); if (!static::has($identity)) { static::set($identity, $model); return $model; } else { $old = static::get($identity); - /** @todo Sync state */ + // sync state $props = $old->getUpdatedProps(); $old->fill($model->getData()); $old->saveState(); - var_dump($props); - foreach ($props as $name => $value) { - $old->$name = $value; - } - $old->saveState(); + $old->fill($props); return $old; } - // return static::get($identity); } public static function unset($identity) { + @[$identity] = static::getIdentity($identity); unset(static::instance()->models[(string) $identity]); return static::instance(); } From 7802ad6169e153db7111529b04e09777a7c53cf5 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 01:49:50 +0300 Subject: [PATCH 29/60] Update ActiveRecord.php --- src/ActiveRecord.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 12203dc..70c98ef 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -114,8 +114,9 @@ public function delete() $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->identityMapRemove(); $this->$pk = null; + $this->saveState(); } return $this; } @@ -126,8 +127,8 @@ public function delete() public function reload() { return ($pv = $this->primaryValue()) - ? static::find($pv) - // ? $this->fill((array) static::find($pv)) + // ? static::find($pv) + ? $this->fill(static::find($pv)->toArray()) : $this; } } From db92baa0e3ea6434c221b14e339f2b3e46cc5d13 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 01:49:56 +0300 Subject: [PATCH 30/60] Update ActiveRecordIdentityTrait.php --- src/Traits/ActiveRecordIdentityTrait.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Traits/ActiveRecordIdentityTrait.php b/src/Traits/ActiveRecordIdentityTrait.php index 9d8eab2..c6b6fcf 100644 --- a/src/Traits/ActiveRecordIdentityTrait.php +++ b/src/Traits/ActiveRecordIdentityTrait.php @@ -16,7 +16,8 @@ trait ActiveRecordIdentityTrait public function identity() { if (!$this->identity) { - $this->identity = ModelIdentity::createFromModel($this); + // $this->identity = ModelIdentity::createFromModel($this); + $this->identity = implode(':', [static::class, $this->primaryValue(), static::getDbName()]); } return $this->identity; } @@ -28,6 +29,6 @@ public function identityMapSave() public function identityMapRemove() { - IdentityMap::unset($this->identity()); + IdentityMap::unset($this); } } From 6f736bd6862bd6cc248bff93cc947ea923ab2279 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 01:53:05 +0300 Subject: [PATCH 31/60] Update IdentityMap.php --- src/IdentityMap.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/IdentityMap.php b/src/IdentityMap.php index 5926dcb..9494ce9 100644 --- a/src/IdentityMap.php +++ b/src/IdentityMap.php @@ -21,11 +21,8 @@ protected static function getIdentity($identity, $model = null) if (func_num_args() > 1 && $model instanceof ActiveRecord) { return [$identity, $model]; } - if ($identity instanceof ActiveRecord) { - return [$identity->identity(), $identity]; - } else { - return [$identity]; - } + return ($identity instanceof ActiveRecord) + ? [$identity->identity(), $identity] : [$identity]; } protected function __construct() From 05629810b11e7ab2dd12644a6c5f10bdb1356d00 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 02:12:09 +0300 Subject: [PATCH 32/60] Delete ModelIdentity.php --- src/Identity/ModelIdentity.php | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/Identity/ModelIdentity.php diff --git a/src/Identity/ModelIdentity.php b/src/Identity/ModelIdentity.php deleted file mode 100644 index e39fbe5..0000000 --- a/src/Identity/ModelIdentity.php +++ /dev/null @@ -1,27 +0,0 @@ -dbname = $dbname; - $this->class = $class; - $this->id = $id; - } - - public function __toString() - { - return implode(':', [$this->dbname, $this->class, $this->id]); - } - - public static function createFromModel(object $model) - { - return ($id = $model->primaryValue()) - ? new static(get_class($model), $id, $model::getDbName()) - : null; - } -} From 8804bd113edb5a2cc371e4e87c7eb9b1363d5f98 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 02:12:57 +0300 Subject: [PATCH 33/60] Update IdentityMap.php --- src/IdentityMap.php | 95 ++++++++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/src/IdentityMap.php b/src/IdentityMap.php index 9494ce9..f846926 100644 --- a/src/IdentityMap.php +++ b/src/IdentityMap.php @@ -1,4 +1,9 @@ + */ namespace Evas\Orm; use Evas\Orm\Identity\ModelIdentity; @@ -6,68 +11,90 @@ 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 static function getIdentity($identity, $model = null) - { - if (func_num_args() > 1 && $model instanceof ActiveRecord) { - return [$identity, $model]; - } - return ($identity instanceof ActiveRecord) - ? [$identity->identity(), $identity] : [$identity]; - } - + /** + * Конструктор. + */ protected function __construct() { $this->resetModels(); } + /** + * Очистка моделей. + */ public function resetModels() { $this->models = []; return $this; } + /** + * Получение количества моделей. + * @return int + */ public static function count(): int { return count(static::instance()->models); } - public static function has($identity) + /** + * Проверка наличия модели в IdentityMap. + * @param ActiveRecord модель + * @return bool + */ + public static function has(ActiveRecord $model): bool { - @[$identity] = static::getIdentity($identity); - return isset(static::instance()->models[(string) $identity]); + return isset(static::instance()->models[$model->identity()]); } - public static function set($identity, $model = null) + /** + * Добавление модели в IdentityMap. + * @param ActiveRecord модель + * @return self + */ + public static function set(ActiveRecord $model) { - @[$identity, $model] = static::getIdentity($identity, $model); - static::instance()->models[(string) $identity] = $model; + static::instance()->models[$model->identity()] = $model; return static::instance(); } - public static function get($identity) + /** + * Получение модели из IdentityMap или null. + * @param ActiveRecord модель + * @return ActiveRecord|null модель или null + */ + public static function get(ActiveRecord $model) { - @[$identity] = static::getIdentity($identity); - return static::instance()->models[(string) $identity] ?? null; + return static::instance()->models[$model->identity()] ?? null; } - public static function getOrSet($identity, $model = null) + /** + * Получение модели с установкой в случае отсутствия. + * @param ActiveRecord модель + * @return ActiveRecord модель + */ + public static function getWithSave(ActiveRecord $model) { - @[$identity, $model] = static::getIdentity($identity, $model); - if (!static::has($identity)) { - static::set($identity, $model); + if (!static::has($model)) { + static::set($model); return $model; } else { - $old = static::get($identity); + $old = static::get($model); // sync state $props = $old->getUpdatedProps(); $old->fill($model->getData()); @@ -77,18 +104,30 @@ public static function getOrSet($identity, $model = null) } } - public static function unset($identity) + /** + * Удаление модели из IdentityMap. + * @param ActiveRecord модель + * @return self + */ + public static function unset(ActiveRecord $model) { - @[$identity] = static::getIdentity($identity); - unset(static::instance()->models[(string) $identity]); + unset(static::instance()->models[$model->identity()]); return static::instance(); } + /** + * Удаление всех моделей из IdentityMap. + * @return self + */ public static function unsetAll() { - static::instance()->resetModels(); + return static::instance()->resetModels(); } + /** + * Приведение IdentityMap к строке. + * @return string + */ public function __toString() { return json_encode(["models_count" => static::count()]); From 28150e550b8f607dcb163d87e6ab9a121b8d9c02 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 02:13:01 +0300 Subject: [PATCH 34/60] Update ActiveRecordIdentityTrait.php --- src/Traits/ActiveRecordIdentityTrait.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Traits/ActiveRecordIdentityTrait.php b/src/Traits/ActiveRecordIdentityTrait.php index c6b6fcf..15344af 100644 --- a/src/Traits/ActiveRecordIdentityTrait.php +++ b/src/Traits/ActiveRecordIdentityTrait.php @@ -6,27 +6,38 @@ */ namespace Evas\Orm\Traits; -use Evas\Orm\Identity\ModelIdentity; +use Evas\Orm\ActiveRecord; use Evas\Orm\IdentityMap; trait ActiveRecordIdentityTrait { + /** @var string идентификатор модели для IdentityMap */ protected $identity; - public function identity() + /** + * Получение идентификатора модели для IdentityMap + * @return string идентификатор модели для IdentityMap + * */ + public function identity(): string { if (!$this->identity) { - // $this->identity = ModelIdentity::createFromModel($this); $this->identity = implode(':', [static::class, $this->primaryValue(), static::getDbName()]); } return $this->identity; } - public function identityMapSave() + /** + * Сохранение модели в IdentityMap. + * @return static + */ + public function identityMapSave(): ActiveRecord { - return IdentityMap::getOrSet($this); + return IdentityMap::getWithSave($this); } + /** + * Удаление модели из IdentityMap. + */ public function identityMapRemove() { IdentityMap::unset($this); From b6fe841432aeb3cf0cc30fb99854c40c4847836f Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Tue, 8 Nov 2022 02:17:05 +0300 Subject: [PATCH 35/60] Update IdentityMap.php --- src/IdentityMap.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/IdentityMap.php b/src/IdentityMap.php index f846926..4f0d732 100644 --- a/src/IdentityMap.php +++ b/src/IdentityMap.php @@ -44,7 +44,7 @@ public function resetModels() } /** - * Получение количества моделей. + * Получение количества моделей в IdentityMap. * @return int */ public static function count(): int @@ -52,6 +52,15 @@ 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 модель @@ -130,6 +139,6 @@ public static function unsetAll() */ public function __toString() { - return json_encode(["models_count" => static::count()]); + return json_encode(['models_count' => static::count(), 'models' => $this->models]); } } From 802a470d2efbbb72c148aaa171094e1684ea53b3 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 00:41:33 +0300 Subject: [PATCH 36/60] rename db & dbname methods --- src/Traits/ActiveRecordDatabaseTrait.php | 6 +++--- src/Traits/ActiveRecordIdentityTrait.php | 2 +- src/Traits/ActiveRecordQueryTrait.php | 6 +++--- src/Traits/ActiveRecordTableTrait.php | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Traits/ActiveRecordDatabaseTrait.php b/src/Traits/ActiveRecordDatabaseTrait.php index 7321913..59ef4b9 100644 --- a/src/Traits/ActiveRecordDatabaseTrait.php +++ b/src/Traits/ActiveRecordDatabaseTrait.php @@ -21,9 +21,9 @@ trait ActiveRecordDatabaseTrait * @param bool использовать ли соединение для записи * @return DatabaseInterface */ - public static function getDb(bool $write = false): DatabaseInterface + public static function db(bool $write = false): DatabaseInterface { - $dbname = static::getDbName($write); + $dbname = static::dbName($write); return App::db($dbname); } @@ -32,7 +32,7 @@ public static function getDb(bool $write = false): DatabaseInterface * @param bool использовать ли соединение для записи * @return string|null */ - public static function getDbName(bool $write = false): ?string + 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 index 15344af..bf31929 100644 --- a/src/Traits/ActiveRecordIdentityTrait.php +++ b/src/Traits/ActiveRecordIdentityTrait.php @@ -21,7 +21,7 @@ trait ActiveRecordIdentityTrait public function identity(): string { if (!$this->identity) { - $this->identity = implode(':', [static::class, $this->primaryValue(), static::getDbName()]); + $this->identity = implode(':', [static::class, $this->primaryValue(), static::dbName()]); } return $this->identity; } diff --git a/src/Traits/ActiveRecordQueryTrait.php b/src/Traits/ActiveRecordQueryTrait.php index 536162e..f963517 100644 --- a/src/Traits/ActiveRecordQueryTrait.php +++ b/src/Traits/ActiveRecordQueryTrait.php @@ -18,7 +18,7 @@ trait ActiveRecordQueryTrait */ public static function query(string $sql, array $values = null) { - $qr = static::getDb()->query($sql, $values); + $qr = static::db()->query($sql, $values); return strstr($qr->stmt()->queryString, 'LIMIT 1') ? $qr->object(static::class) : $qr->objectAll(static::class); @@ -34,7 +34,7 @@ public static function query(string $sql, array $values = null) public static function __callStatic(string $name, array $args = null) { if (method_exists(QueryBuilder::class, $name)) { - $db = static::getDb(); + $db = static::db(); return (new QueryBuilder($db, static::class))->$name(...$args); } throw new \BadMethodCallException(sprintf( @@ -50,7 +50,7 @@ public static function __callStatic(string $name, array $args = null) public static function find($ids) { $ids = func_num_args() > 1 ? func_get_args() : $ids; - $db = static::getDb(); + $db = static::db(); return (new QueryBuilder($db, static::class))->find($ids); } } diff --git a/src/Traits/ActiveRecordTableTrait.php b/src/Traits/ActiveRecordTableTrait.php index c9c55d2..631f60f 100644 --- a/src/Traits/ActiveRecordTableTrait.php +++ b/src/Traits/ActiveRecordTableTrait.php @@ -46,7 +46,7 @@ public static function tableName(): string */ public static function table(bool $write = false): TableInterface { - return static::getDb($write)->table(static::tableName()); + return static::db($write)->table(static::tableName()); } /** From febfcc8d0613fdd10064744fbcba15f0cbf9a7b0 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:29:06 +0300 Subject: [PATCH 37/60] Create ActiveRecordRelationsTrait.php --- src/Traits/ActiveRecordRelationsTrait.php | 111 ++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/Traits/ActiveRecordRelationsTrait.php diff --git a/src/Traits/ActiveRecordRelationsTrait.php b/src/Traits/ActiveRecordRelationsTrait.php new file mode 100644 index 0000000..0521d15 --- /dev/null +++ b/src/Traits/ActiveRecordRelationsTrait.php @@ -0,0 +1,111 @@ + + */ +namespace Evas\Orm\Traits; + +use Evas\Base\Help\PhpHelp; +use Evas\Orm\Relation; + +trait ActiveRecordRelationsTrait +{ + /** @static Relation[] связи */ + // protected static $relations; + protected static $relations; + + /** + * Описание связей модели. + * @return array + */ + protected 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); + } + + + /** + * Инициализация связей. + * @throws \InvalidArgumentException + */ + protected static function initRelations() + { + if (!is_null(static::$relations)) return; + $relations = static::relations(); + if ($relations) foreach ($relations as $name => &$relation) { + if (!$relation instanceof Relation) { + throw new \InvalidArgumentException(sprintf( + 'Relation must be instance of %s, %s given', + Relation::class, PhpHelp::getType($relation) + )); + } + $relation->setName($name); + } + static::$relations = &$relations; + } + + /** + * Получение связи по имени. + * @param string имя связи + * @return Relation|null + */ + public static function getRelation(string $name): ?Relation + { + static::initRelations(); + return static::$relations[$name] ?? null; + // return static::relations()[$name] ?? null; + } + + /** + * Проверка наличия связи. + * @param string имя связи + * @return bool + */ + public static function hasRelation(string $name): bool + { + static::initRelations(); + return isset(static::$relations[$name]); + // return isset(static::relations()[$name]); + } +} From cb1ea8894da561dfe83e1b9466c24e6ac6b0517a Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:29:10 +0300 Subject: [PATCH 38/60] Create ActiveRecordRelatedsTrait.php --- src/Traits/ActiveRecordRelatedsTrait.php | 90 ++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/Traits/ActiveRecordRelatedsTrait.php diff --git a/src/Traits/ActiveRecordRelatedsTrait.php b/src/Traits/ActiveRecordRelatedsTrait.php new file mode 100644 index 0000000..d1f26ce --- /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; + } +} From 4b3c0f61e3ffc960662fe054618f48e531ddb387 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:29:16 +0300 Subject: [PATCH 39/60] Update ActiveRecordDataTrait.php --- src/Traits/ActiveRecordDataTrait.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Traits/ActiveRecordDataTrait.php b/src/Traits/ActiveRecordDataTrait.php index 4124a13..6ccebe5 100644 --- a/src/Traits/ActiveRecordDataTrait.php +++ b/src/Traits/ActiveRecordDataTrait.php @@ -68,11 +68,11 @@ public function __set(string $name, $value) */ public function __get(string $name) { - // if (static::hasRelation($name)) { - // return $this->getRelatedCollection($name); - // } else { + if (static::hasRelation($name)) { + return $this->getRelatedCollection($name); + } else { return $this->modelData[$name] ?? null; - // } + } } /** From f6515cb7473168faa9d44b23208a49eb519af64b Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:29:31 +0300 Subject: [PATCH 40/60] Create Relation.php --- src/Relation.php | 139 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/Relation.php diff --git a/src/Relation.php b/src/Relation.php new file mode 100644 index 0000000..493696e --- /dev/null +++ b/src/Relation.php @@ -0,0 +1,139 @@ + + */ +namespace Evas\Orm; + +use Evas\Orm\ActiveRecord; + +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; + // $this->foreignFullKey = "$this->name.$this->foreignKey"; + return $this; + } + + public function foreignColumn(string $column, bool $useName = false) + { + return ($useName ? $this->name : $this->foreignTable) .'.'. $column; + } + + public function foreignPrimary(bool $useName = false) + { + return $this->foreignColumn($this->foreignModel::primaryKey(), $useName); + } + + public function foreignKey(bool $useName = false) + { + return $this->foreignColumn($this->foreignKey, $useName); + } + + /** + * Добавление связи с моделью. + * @param ActiveRecord модель + * @param array данные внешней модели + * @return ActiveRecord модель + */ + public function addRelated(ActiveRecord $model, array $foreignData) + { + return $model->addRelated($this->name, new $this->foreignModel($foreignData)); + } + + public function isOne(): bool + { + return in_array($this->type, ['hasOne', 'belongsTo']); + } + + public function isMany(): bool + { + return !$this->isOne(); + } + + + 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); + } +} From 63ec9ca57484953e30c7215bb3ca2cede22fcd67 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:29:39 +0300 Subject: [PATCH 41/60] Create RelatedCollection.php --- src/RelatedCollection.php | 96 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/RelatedCollection.php 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; + } +} From c0f16a0e5047a7fea1fd84dbca944bb50d505513 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:30:03 +0300 Subject: [PATCH 42/60] ActiveRecord: add relations & relateds --- src/ActiveRecord.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 70c98ef..ef1bdfc 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -11,6 +11,8 @@ 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; @@ -27,6 +29,10 @@ class ActiveRecord implements \JsonSerializable use ActiveRecordDataTrait; + use ActiveRecordRelatedsTrait; + + use ActiveRecordRelationsTrait; + use ActiveRecordStateTrait; use ActiveRecordTableTrait; From 588fc913cb94e26320f27f48563e0154a3f330eb Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:30:11 +0300 Subject: [PATCH 43/60] Update ActiveRecord.php --- src/ActiveRecord.php | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index ef1bdfc..be4274e 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -68,26 +68,37 @@ public function __construct(array $props = null) */ public function save() { - $props = $this->getUpdatedProps(); - if (empty($props)) { + if (empty($this->getUpdatedProps())) { $this->hook('nothingSave'); return $this; } - $this->hook('beforeSave', $props); + $this->hook('beforeSave'); $pk = static::primaryKey(); if ($this->isStateHasPrimaryValue()) { - $this->hook('beforeUpdate', $props); - static::table(true)->where($pk, $this->$pk)->update($props); - $this->hook('afterUpdate', $props); + $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); + } } else { - $this->hook('beforeInsert', $props); - static::table(true)->insert($props); - $this->$pk = static::lastInsertId(); - if (empty($this->$pk)) { - throw new LastInsertIdUndefinedException(); + $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('afterInsert', $props); } $this->hook('afterSave', $props); // return static::getDb()->identityMapUpdate($this, $pk); From d9f1517e8c2d26017e759cdbd51f6d822db44070 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:30:27 +0300 Subject: [PATCH 44/60] Create QueryBuilderRelationsTrait.php --- src/Traits/QueryBuilderRelationsTrait.php | 244 ++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/Traits/QueryBuilderRelationsTrait.php diff --git a/src/Traits/QueryBuilderRelationsTrait.php b/src/Traits/QueryBuilderRelationsTrait.php new file mode 100644 index 0000000..b7147c2 --- /dev/null +++ b/src/Traits/QueryBuilderRelationsTrait.php @@ -0,0 +1,244 @@ + + */ +namespace Evas\Orm\Traits; + +use Evas\Orm\ActiveRecord; +use Evas\Orm\Relation; + +trait QueryBuilderRelationsTrait +{ + /** @var string разделитель для полей связанных записей */ + public static $relationDataSeparator = '_-_'; + + protected $withs = []; + protected $withsAfter = []; + + protected function getRelation(string $name): ?Relation + { + return $this->model::getRelation($name); + } + + protected function addWith(string $name, $columns, $query = null) + { + $relation = $this->getRelation($name); + if ($relation) { + if ($relation->isOne()) $list = &$this->withs; + else $list = &$this->withsAfter; + // if (!isset($list[$relation->name])) { + // $list[$relation->name] = [$relation, $columns, $query]; + // } + $list[$relation->name] = [$relation, $columns, $query]; + } + echo title('addWith: ' . $name, 3);// . dumpOrm($relation); + // echo dumpOrm($this->model::$relations); + return $this; + } + + public function with(...$props) + { + foreach ($props as &$prop) { + if (is_string($prop)) { + @[$name, $columns] = explode(':', $prop); + $this->addWith($name, $columns); + + } else if (is_array($prop)) { + foreach ($prop as $name => &$sub) { + $columns = null; + $query = null; + + if (is_string($name)) { + if (is_string($sub) || is_array($sub)) { + $columns = $sub; + } else if (is_callable($sub)) { + @[$name, $columns] = explode(':', $name); + $relation = $this->getRelation($name); + if (!$relation) return; + $query = new static($this->db, $relation->foreignModel); + $sub($query); + } + + } else if (is_string($sub)) { + @[$name, $columns] = explode(':', $sub); + } else { + throw new \InvalidArgumentException(sprintf( + 'Incorrect arguments in method %s()', + __METHOD__ + )); + } + $this->addWith($name, $columns, $query); + } + } + } + return $this; + } + + protected function addHas( + bool $isNot, bool $isWith, string $relationName, + $operator = null, $value = null + ) { + @[$relationName, $columns] = explode(':', $relationName); + $relation = $this->getRelation($relationName); + if ($relation) { + $this->select(static::prepareModelColumns($this->columns, $this->model)); + if ($columns) { + $columns = explode(',', $columns); + foreach ($columns as $column) { + $this->whereNotNull($relation->foreignColumn($column, true)); + } + } + if (func_num_args() > 3) { + if ($this->isQueryable($operator)) { + if ($operator instanceof \Closure) { + call_user_func($operator, $operator = $this->newQuery()); + } + foreach ($operator->wheres as $where) { + if (isset($where['columns'])) + foreach ($where['columns'] as &$column) { + $this->addColumnTablePrefix($column, $relationName); + } + if (isset($where['column'])) + $where['column'] = $this->addColumnTablePrefix( + $where['column'], $relationName + ); + $this->pushWhere($where['type'], $where); + } + } else { + $this->prepareValueAndOperator( + $value, $operator, func_num_args() === 4 + ); + $foreignPrimary = $relation->foreignPrimary(true); + if ($columns) { + // isHas + $this->where( + $relation->foreignColumn($column, true), + $operator, $value + ); + } else { + $this->count($foreignPrimary); + $this->havingAggregate( + 'count', $foreignPrimary, $operator, $value + ); + if ($value === 0) $isNot = true; + } + } + } + + $args = [ + $relation->foreignTable, $relation->name, + $relation->foreignKey(), $relation->localFullKey + ]; + + if ($isNot) { + if (func_num_args() > 3) { + $this->leftJoinSub(...$args); + $this->groupBy($relation->localFullKey); + } else { + $this->leftOuterJoinSub(...$args); + $this->whereNull($relation->foreignKey()); + } + } else { + $this->joinSub(...$args); + $this->groupBy($relation->localFullKey); + } + } + + return $this; + } + + public function has(string $relationName, $operator = null, $value = null) + { + return $this->addHas(false, false, ...func_get_args()); + } + + public function notHas(string $relationName, $operator = null, $value = null) + { + return $this->addHas(true, false, ...func_get_args()); + } + + + protected static function prepareColumns($columns): ?array + { + return (empty($columns) || '*' == $columns + || (is_array($columns) && in_array('*', $columns))) + ? null : explode(',', str_replace(' ', '', $columns)); + } + + protected static function prepareModelColumns( + $columns, string $model, string $asPrefix = null + ) { + $columns = static::prepareColumns($columns) ?? $model::columns(); + $table = $model::tableName(); + $keys = []; + foreach ($columns as &$column) { + if (!empty($asPrefix)) { + $as = $asPrefix . static::$relationDataSeparator . $column; + $col = "{$asPrefix}.{$column}"; + } else { + $as = $column; + $col = "{$table}.{$column}"; + } + $keys[$as] = $col; + } + return $keys; + } + + protected function addColumnTablePrefix(string $column, string $table) + { + $parts = explode('.', $column); + if (count($parts) < 2) { + array_unshift($parts, $table); + $column = implode('.', $parts); + } + return $column; + } + + protected function applyWiths() + { + // $withs = array_filter($this->withs, fn($with) => $with->isOne()); + if (1 > count($this->withs)) return; + echo title('applyWiths', 3); + $columns = static::prepareModelColumns($this->columns, $this->model); + $this->select($columns); + foreach ($this->withs as [$relation, $columns, $query]) { + $this->applyWith($relation, $columns, $query); + } + } + + protected function applyWith( + Relation $relation, $columns = null, self $query = null + ) { + $columns = static::prepareModelColumns( + $columns, $relation->foreignModel, $relation->name + ); + $this->select($columns); + $this->leftJoinSub( + $query ?? $relation->foreignTable, + $relation->name, + "{$relation->name}.{$relation->foreignKey}", + $relation->localFullKey + ); + return $this; + } + + protected function parseWiths(ActiveRecord &$model) + { + if (1 > count($this->withs)) return; + // echo title('parseWiths', 3) . dumpOrm($model); + $foreigns = []; + foreach ($model->toArray() as $key => $value) { + @[$fname, $fkey] = explode(static::$relationDataSeparator, $key, 2); + if ($fkey && in_array($fname, array_keys($this->withs))) { + $foreigns[$fname][$fkey] = $value; + unset($model->$key); + } + } + foreach ($foreigns as $fname => $subs) { + // $this->withs[$fname][0]->addRelated($result, $subs); + $model->addRelatedData($fname, $subs); + } + } +} From 2fbfc882db48a16232e2a89ecb39ac67ff3e0100 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:30:38 +0300 Subject: [PATCH 45/60] Update QueryBuilder.php --- src/QueryBuilder.php | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 8126842..86ced68 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -8,9 +8,12 @@ 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; @@ -61,12 +64,15 @@ protected function primaryKey(): string */ public function get($columns = null): array { - if ($columns) $this->addSelect(...func_get_args()); - $result = $this->query()->objectAll($this->model); - foreach ($result as &$model) { + $this->applyWiths(); + if ($columns) $this->select(...func_get_args()); + $models = $this->query()->objectAll($this->model); + foreach ($models as &$model) { $model = $model->identityMapSave(); + if (0 < count($this->withs)) $this->parseWiths($model); } - return $result; + $models = array_unique($models); + return $models; } /** @@ -76,8 +82,14 @@ public function get($columns = null): array */ public function one($columns = null) { - if ($columns) $this->addSelect(...func_get_args()); + $this->applyWiths(); + if ($columns) $this->select(...func_get_args()); $model = $this->limit(1)->query()->object($this->model); - return is_null($model) ? $model : $model->identityMapSave(); + if ($model) { + if (0 < count($this->withs)) $this->parseWiths($model); + $model = $model->identityMapSave(); + } + return $model; + // return is_null($model) ? $model : $model->identityMapSave(); } } From 6e8caa1249aeaa210d033ea7eee4b5946c4823f9 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 21 Nov 2022 01:30:43 +0300 Subject: [PATCH 46/60] Update ActiveRecordStateTrait.php --- src/Traits/ActiveRecordStateTrait.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Traits/ActiveRecordStateTrait.php b/src/Traits/ActiveRecordStateTrait.php index 550ac06..d7abcb9 100644 --- a/src/Traits/ActiveRecordStateTrait.php +++ b/src/Traits/ActiveRecordStateTrait.php @@ -63,11 +63,23 @@ public function getUpdatedProps(): array return $props; } else { $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 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 '
'; + return $updated; + // return array_merge( + // array_fill_keys(array_keys(array_diff($state ?? [], $props)), null), + // array_diff_assoc($props, $state ?? []) + // ); } } } From 5d36842abbe09fb0089e8d56c717cc27d0baeda9 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sat, 26 Nov 2022 13:38:40 +0300 Subject: [PATCH 47/60] Create RelationsMap.php --- src/RelationsMap.php | 131 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/RelationsMap.php 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]); + } +} From af3e01aaa76ceff8dd3a39a3f221a45fe035a2eb Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sat, 26 Nov 2022 13:38:48 +0300 Subject: [PATCH 48/60] Update ActiveRecordRelationsTrait.php --- src/Traits/ActiveRecordRelationsTrait.php | 33 +++-------------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/Traits/ActiveRecordRelationsTrait.php b/src/Traits/ActiveRecordRelationsTrait.php index 0521d15..047dc8f 100644 --- a/src/Traits/ActiveRecordRelationsTrait.php +++ b/src/Traits/ActiveRecordRelationsTrait.php @@ -8,13 +8,10 @@ use Evas\Base\Help\PhpHelp; use Evas\Orm\Relation; +use Evas\Orm\RelationsMap; trait ActiveRecordRelationsTrait { - /** @static Relation[] связи */ - // protected static $relations; - protected static $relations; - /** * Описание связей модели. * @return array @@ -65,26 +62,6 @@ public static function belongsTo( } - /** - * Инициализация связей. - * @throws \InvalidArgumentException - */ - protected static function initRelations() - { - if (!is_null(static::$relations)) return; - $relations = static::relations(); - if ($relations) foreach ($relations as $name => &$relation) { - if (!$relation instanceof Relation) { - throw new \InvalidArgumentException(sprintf( - 'Relation must be instance of %s, %s given', - Relation::class, PhpHelp::getType($relation) - )); - } - $relation->setName($name); - } - static::$relations = &$relations; - } - /** * Получение связи по имени. * @param string имя связи @@ -92,9 +69,7 @@ protected static function initRelations() */ public static function getRelation(string $name): ?Relation { - static::initRelations(); - return static::$relations[$name] ?? null; - // return static::relations()[$name] ?? null; + return RelationsMap::getRelation(static::class, $name); } /** @@ -104,8 +79,6 @@ public static function getRelation(string $name): ?Relation */ public static function hasRelation(string $name): bool { - static::initRelations(); - return isset(static::$relations[$name]); - // return isset(static::relations()[$name]); + return RelationsMap::hasRelation(static::class, $name); } } From c32b8aa9f2e4866693a7cfeb5234676c6aff8a7a Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sat, 26 Nov 2022 13:38:58 +0300 Subject: [PATCH 49/60] Update Relation.php --- src/Relation.php | 58 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/Relation.php b/src/Relation.php index 493696e..759206e 100644 --- a/src/Relation.php +++ b/src/Relation.php @@ -7,6 +7,7 @@ namespace Evas\Orm; use Evas\Orm\ActiveRecord; +use Evas\Orm\QueryBuilder; class Relation { @@ -87,25 +88,62 @@ protected static function generateForeignKey(string $localModel): string public function setName(string $name) { $this->name = $name; - // $this->foreignFullKey = "$this->name.$this->foreignKey"; return $this; } - public function foreignColumn(string $column, bool $useName = false) + /** + * Получение внешнего столбца. + * @param string столбец + * @param bool|null исользовать ли имя связи вместо имение таблицы + * @return string + */ + public function foreignColumn(string $column, bool $useName = false): string { return ($useName ? $this->name : $this->foreignTable) .'.'. $column; } - public function foreignPrimary(bool $useName = false) + /** + * Получение внешнего первичного ключа. + * @param bool|null исользовать ли имя связи вместо имение таблицы + * @return string + */ + public function foreignPrimary(bool $useName = false): string { return $this->foreignColumn($this->foreignModel::primaryKey(), $useName); } - public function foreignKey(bool $useName = false) + /** + * Получение внешнего ключа связи. + * @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 localKey(bool $useFull = true): string + { + return ($useFull ? ($this->localTable.'.') : '') . $this->localKey; + } + + /** + * Получение аргументов для 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 модель @@ -117,17 +155,29 @@ 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([ From 0c198de031a8e94df8a7af1e70ae4d5e6cbcf019 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sat, 26 Nov 2022 13:39:23 +0300 Subject: [PATCH 50/60] Update QueryBuilderRelationsTrait.php --- src/Traits/QueryBuilderRelationsTrait.php | 28 +++++++++++------------ 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Traits/QueryBuilderRelationsTrait.php b/src/Traits/QueryBuilderRelationsTrait.php index b7147c2..c9b52a6 100644 --- a/src/Traits/QueryBuilderRelationsTrait.php +++ b/src/Traits/QueryBuilderRelationsTrait.php @@ -22,7 +22,7 @@ protected function getRelation(string $name): ?Relation return $this->model::getRelation($name); } - protected function addWith(string $name, $columns, $query = null) + protected function addWith(string $name, $columns, self $query = null) { $relation = $this->getRelation($name); if ($relation) { @@ -33,7 +33,7 @@ protected function addWith(string $name, $columns, $query = null) // } $list[$relation->name] = [$relation, $columns, $query]; } - echo title('addWith: ' . $name, 3);// . dumpOrm($relation); + // echo title('addWith: ' . $name, 3);// . dumpOrm($relation); // echo dumpOrm($this->model::$relations); return $this; } @@ -127,22 +127,19 @@ protected function addHas( } } - $args = [ - $relation->foreignTable, $relation->name, - $relation->foreignKey(), $relation->localFullKey - ]; + $args = $relation->joinArgs(null, false); if ($isNot) { if (func_num_args() > 3) { $this->leftJoinSub(...$args); - $this->groupBy($relation->localFullKey); + $this->groupBy($relation->localKey(true)); } else { $this->leftOuterJoinSub(...$args); $this->whereNull($relation->foreignKey()); } } else { $this->joinSub(...$args); - $this->groupBy($relation->localFullKey); + $this->groupBy($relation->localKey(true)); } } @@ -200,7 +197,7 @@ protected function applyWiths() { // $withs = array_filter($this->withs, fn($with) => $with->isOne()); if (1 > count($this->withs)) return; - echo title('applyWiths', 3); + // echo title('applyWiths', 3); $columns = static::prepareModelColumns($this->columns, $this->model); $this->select($columns); foreach ($this->withs as [$relation, $columns, $query]) { @@ -215,12 +212,13 @@ protected function applyWith( $columns, $relation->foreignModel, $relation->name ); $this->select($columns); - $this->leftJoinSub( - $query ?? $relation->foreignTable, - $relation->name, - "{$relation->name}.{$relation->foreignKey}", - $relation->localFullKey - ); + // $this->leftJoinSub( + // $query ?? $relation->foreignTable, + // $relation->name, + // "{$relation->name}.{$relation->foreignKey}", + // $relation->localKey(true) + // ); + $this->leftJoinSub(...$relation->joinArgs($query, true)); return $this; } From a84f37583a813b4f49f1ef03c8a6e967b4c886f9 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sat, 26 Nov 2022 13:41:55 +0300 Subject: [PATCH 51/60] Update ActiveRecordRelationsTrait.php --- src/Traits/ActiveRecordRelationsTrait.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Traits/ActiveRecordRelationsTrait.php b/src/Traits/ActiveRecordRelationsTrait.php index 047dc8f..308c48f 100644 --- a/src/Traits/ActiveRecordRelationsTrait.php +++ b/src/Traits/ActiveRecordRelationsTrait.php @@ -32,7 +32,9 @@ protected static function relations(): array public static function hasMany( string $foreignModel, string $foreignKey = null, string $localKey = null ): Relation { - return new Relation('hasMany', static::class, $foreignModel, $foreignKey, $localKey); + return new Relation( + 'hasMany', static::class, $foreignModel, $foreignKey, $localKey + ); } /** @@ -45,7 +47,9 @@ public static function hasMany( public static function hasOne( string $foreignModel, string $foreignKey = null, string $localKey = null ): Relation { - return new Relation('hasOne', static::class, $foreignModel, $foreignKey, $localKey); + return new Relation( + 'hasOne', static::class, $foreignModel, $foreignKey, $localKey + ); } /** @@ -58,7 +62,9 @@ public static function hasOne( public static function belongsTo( string $foreignModel, string $foreignKey = null, string $localKey = null ): Relation { - return new Relation('belongsTo', static::class, $foreignModel, $foreignKey, $localKey); + return new Relation( + 'belongsTo', static::class, $foreignModel, $foreignKey, $localKey + ); } From 9feda9ebaf26b4e92f0cb94b0bf38eb235514d40 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 11 Dec 2022 23:54:33 +0300 Subject: [PATCH 52/60] Update Relation.php --- src/Relation.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Relation.php b/src/Relation.php index 759206e..d13d4a2 100644 --- a/src/Relation.php +++ b/src/Relation.php @@ -122,6 +122,16 @@ 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 исользовать ли полный ключ @@ -129,7 +139,7 @@ public function foreignKey(bool $useName = false): string */ public function localKey(bool $useFull = true): string { - return ($useFull ? ($this->localTable.'.') : '') . $this->localKey; + return $this->localColumn($this->localKey, $useFull); } /** From 08aa2e48ed4099d225dcde2c3e96cc4b366cc346 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 11 Dec 2022 23:54:38 +0300 Subject: [PATCH 53/60] Update ActiveRecordRelationsTrait.php --- src/Traits/ActiveRecordRelationsTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/ActiveRecordRelationsTrait.php b/src/Traits/ActiveRecordRelationsTrait.php index 308c48f..ffbd620 100644 --- a/src/Traits/ActiveRecordRelationsTrait.php +++ b/src/Traits/ActiveRecordRelationsTrait.php @@ -16,7 +16,7 @@ trait ActiveRecordRelationsTrait * Описание связей модели. * @return array */ - protected static function relations(): array + public static function relations(): array { return []; } From 4ebcc14a2556cb5c057b7f2889c63c1f3ca37329 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 11 Dec 2022 23:54:52 +0300 Subject: [PATCH 54/60] Update QueryBuilderRelationsTrait.php --- src/Traits/QueryBuilderRelationsTrait.php | 229 +++++----------------- 1 file changed, 49 insertions(+), 180 deletions(-) diff --git a/src/Traits/QueryBuilderRelationsTrait.php b/src/Traits/QueryBuilderRelationsTrait.php index c9b52a6..2f044a9 100644 --- a/src/Traits/QueryBuilderRelationsTrait.php +++ b/src/Traits/QueryBuilderRelationsTrait.php @@ -8,160 +8,41 @@ use Evas\Orm\ActiveRecord; use Evas\Orm\Relation; +use Evas\Orm\Traits\QueryBuilderRelationsHasTrait; +use Evas\Orm\Traits\QueryBuilderRelationsWithTrait; trait QueryBuilderRelationsTrait { + use QueryBuilderRelationsHasTrait; + use QueryBuilderRelationsWithTrait; + /** @var string разделитель для полей связанных записей */ public static $relationDataSeparator = '_-_'; - protected $withs = []; - protected $withsAfter = []; - protected function getRelation(string $name): ?Relation { return $this->model::getRelation($name); } - protected function addWith(string $name, $columns, self $query = null) - { - $relation = $this->getRelation($name); - if ($relation) { - if ($relation->isOne()) $list = &$this->withs; - else $list = &$this->withsAfter; - // if (!isset($list[$relation->name])) { - // $list[$relation->name] = [$relation, $columns, $query]; - // } - $list[$relation->name] = [$relation, $columns, $query]; - } - // echo title('addWith: ' . $name, 3);// . dumpOrm($relation); - // echo dumpOrm($this->model::$relations); - return $this; - } - - public function with(...$props) - { - foreach ($props as &$prop) { - if (is_string($prop)) { - @[$name, $columns] = explode(':', $prop); - $this->addWith($name, $columns); - - } else if (is_array($prop)) { - foreach ($prop as $name => &$sub) { - $columns = null; - $query = null; - - if (is_string($name)) { - if (is_string($sub) || is_array($sub)) { - $columns = $sub; - } else if (is_callable($sub)) { - @[$name, $columns] = explode(':', $name); - $relation = $this->getRelation($name); - if (!$relation) return; - $query = new static($this->db, $relation->foreignModel); - $sub($query); - } - - } else if (is_string($sub)) { - @[$name, $columns] = explode(':', $sub); - } else { - throw new \InvalidArgumentException(sprintf( - 'Incorrect arguments in method %s()', - __METHOD__ - )); - } - $this->addWith($name, $columns, $query); - } - } - } - return $this; - } - - protected function addHas( - bool $isNot, bool $isWith, string $relationName, - $operator = null, $value = null - ) { - @[$relationName, $columns] = explode(':', $relationName); - $relation = $this->getRelation($relationName); - if ($relation) { - $this->select(static::prepareModelColumns($this->columns, $this->model)); - if ($columns) { - $columns = explode(',', $columns); - foreach ($columns as $column) { - $this->whereNotNull($relation->foreignColumn($column, true)); - } - } - if (func_num_args() > 3) { - if ($this->isQueryable($operator)) { - if ($operator instanceof \Closure) { - call_user_func($operator, $operator = $this->newQuery()); - } - foreach ($operator->wheres as $where) { - if (isset($where['columns'])) - foreach ($where['columns'] as &$column) { - $this->addColumnTablePrefix($column, $relationName); - } - if (isset($where['column'])) - $where['column'] = $this->addColumnTablePrefix( - $where['column'], $relationName - ); - $this->pushWhere($where['type'], $where); - } - } else { - $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 4 - ); - $foreignPrimary = $relation->foreignPrimary(true); - if ($columns) { - // isHas - $this->where( - $relation->foreignColumn($column, true), - $operator, $value - ); - } else { - $this->count($foreignPrimary); - $this->havingAggregate( - 'count', $foreignPrimary, $operator, $value - ); - if ($value === 0) $isNot = true; - } - } - } - - $args = $relation->joinArgs(null, false); - - if ($isNot) { - if (func_num_args() > 3) { - $this->leftJoinSub(...$args); - $this->groupBy($relation->localKey(true)); - } else { - $this->leftOuterJoinSub(...$args); - $this->whereNull($relation->foreignKey()); - } - } else { - $this->joinSub(...$args); - $this->groupBy($relation->localKey(true)); - } - } - - return $this; - } - - public function has(string $relationName, $operator = null, $value = null) - { - return $this->addHas(false, false, ...func_get_args()); - } - - public function notHas(string $relationName, $operator = null, $value = null) - { - return $this->addHas(true, false, ...func_get_args()); - } - protected static function prepareColumns($columns): ?array { - return (empty($columns) || '*' == $columns - || (is_array($columns) && in_array('*', $columns))) - ? null : explode(',', str_replace(' ', '', $columns)); + // if (empty($columns) || '*' == $columns) { + // return null; + // } + // if (is_array($columns)) { + // if (in_array('*', $columns)) return null; + // else return $columns; + // } + + return (empty($columns) || '*' === $columns) ? null : ( + is_array($columns) ? (in_array('*', $columns) ? null : $columns) + : explode(',', str_replace(' ', '', $columns)) + ); + + // return (empty($columns) || '*' == $columns + // || (is_array($columns) && in_array('*', $columns))) + // ? null : explode(',', str_replace(' ', '', $columns)); } protected static function prepareModelColumns( @@ -193,50 +74,38 @@ protected function addColumnTablePrefix(string $column, string $table) return $column; } - protected function applyWiths() + protected function applyRelationsBefore() { - // $withs = array_filter($this->withs, fn($with) => $with->isOne()); - if (1 > count($this->withs)) return; - // echo title('applyWiths', 3); - $columns = static::prepareModelColumns($this->columns, $this->model); - $this->select($columns); - foreach ($this->withs as [$relation, $columns, $query]) { - $this->applyWith($relation, $columns, $query); - } - } - - protected function applyWith( - Relation $relation, $columns = null, self $query = null - ) { - $columns = static::prepareModelColumns( - $columns, $relation->foreignModel, $relation->name - ); - $this->select($columns); - // $this->leftJoinSub( - // $query ?? $relation->foreignTable, - // $relation->name, - // "{$relation->name}.{$relation->foreignKey}", - // $relation->localKey(true) - // ); - $this->leftJoinSub(...$relation->joinArgs($query, true)); - return $this; + // // if (1 > count($this->withOne) && 1 > count($this->has)) return; + // if (1 > count($this->withs) && 1 > count($this->has)) return; + // $columns = static::prepareModelColumns($this->columns, $this->model); + // $this->select($columns); + // $this->applyWiths(); + // $this->applyHases(); } - protected function parseWiths(ActiveRecord &$model) + protected function applyRelationsAfter(array $ids, array &$models) { if (1 > count($this->withs)) return; - // echo title('parseWiths', 3) . dumpOrm($model); - $foreigns = []; - foreach ($model->toArray() as $key => $value) { - @[$fname, $fkey] = explode(static::$relationDataSeparator, $key, 2); - if ($fkey && in_array($fname, array_keys($this->withs))) { - $foreigns[$fname][$fkey] = $value; - unset($model->$key); - } - } - foreach ($foreigns as $fname => $subs) { - // $this->withs[$fname][0]->addRelated($result, $subs); - $model->addRelatedData($fname, $subs); - } + $this->applyWiths($ids, $models); + // if (1 > count($this->withMany)) return; + // foreach ($this->withMany as [$relation, $columns, $query]) { + // $qb = (new static($this->db, $relation->foreignModel)); + // $subModels = $qb->whereIn($relation->foreignKey, $ids)->get(); + // if (!$subModels) continue; + // $ids = []; + // foreach ($subModels as $subModel) { + // $ids[] = $subModel->primaryValue(); + // foreach ($models as $model) { + // if ($model->{$relation->localKey} == $subModel->{$relation->foreignKey}) { + // $model->addRelated($relation->name, $subModel); + // break; + // } + // } + // } + // if (0 < count($ids)) { + // $this->applyRelationsAfter($ids, $subModels); + // } + // } } } From cc724219226b2aa81a9c1d3e6b5a07da3cfc26b9 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 11 Dec 2022 23:54:58 +0300 Subject: [PATCH 55/60] Create QueryBuilderRelationsHasTrait.php --- src/Traits/QueryBuilderRelationsHasTrait.php | 218 +++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 src/Traits/QueryBuilderRelationsHasTrait.php diff --git a/src/Traits/QueryBuilderRelationsHasTrait.php b/src/Traits/QueryBuilderRelationsHasTrait.php new file mode 100644 index 0000000..0e7e385 --- /dev/null +++ b/src/Traits/QueryBuilderRelationsHasTrait.php @@ -0,0 +1,218 @@ +has[] = func_get_args(); + return $this; + } + + public function has(string $relationName, $operator = null, $value = null) + { + return $this->addHas(false, false, ...func_get_args()); + } + + public function orHas(string $relationName, $operator = null, $value = null) + { + return $this->addHas(true, false, ...func_get_args()); + } + + public function notHas(string $relationName, $operator = null, $value = null) + { + return $this->addHas(false, true, ...func_get_args()); + } + + public function orNotHas(string $relationName, $operator = null, $value = null) + { + return $this->addHas(true, true, ...func_get_args()); + } + + protected function applyHases() + { + if (1 > count($this->has)) return; + // $columns = static::prepareModelColumns($this->columns, $this->model); + // $this->select($columns); + foreach ($this->has as $has) { + $this->applyHas(...$has); + } + } + + protected function applyHas( + bool $isOr, bool $isNot, string $relationName, $operator = null, $value = null + ) { + $relationNames = explode('.', $relationName); + $model = $this->model; + $relations = []; + foreach ($relationNames as $name) { + $relation = $model::getRelation($name); + $model = $relation->foreignModel; + $relations[] = $relation; + } + if (!count($relations)) return; + + if ($hasCount = func_num_args() > 3) { + $this->prepareValueAndOperator( + $value, $operator, func_num_args() === 4 + ); + } + + $relations = array_reverse($relations); + $query = $this->realApplyHas($relations, $operator, $value, $hasCount); + + + if (count($relations) < 2 && $hasCount) { + $this->where($query, $operator, $value); + echo dumpOrm($this->getSql()); + echo dumpOrm($this->getBindings()); + } else { + $this->whereExists($query); + } + + // if (in_array($operator, ['<', '<='])) { + // $query = $this->realApplyNotHas($relations, $operator); + // $this->orWhereNotExists($query); + // } + + // $this->resetSelect('*'); + // $this->whereRaw('EXISTS (SELECT COUNT(`id`) AS `count_id` FROM `company` WHERE `user`.`id` = `company`.`user_id` AND (SELECT COUNT(`id`) AS `count_id` FROM `user` WHERE `company`.`user_id` = `user`.`id`) > 100)'); + } + + protected function realApplyHas($relations, $operator, $value, bool $hasCount = false) + { + $query = null; + foreach ($relations as $i => $relation) { + $query = function ($q) use ( + $relation, $query, $value, $operator, $hasCount, $i + ) { + $q->from($relation->foreignTable); + $q->whereColumn($relation->localKey(true), $relation->foreignKey(false)); + if ($query) { + if (1 == $i && $hasCount) { + $q->where($query, $operator, $value); + } else { + $q->whereExists($query); + } + } else if (0 == $i && $hasCount) { + $q->count('id'); + } + }; + } + return $query; + } + + protected function realApplyNotHas($relations, $operator, bool $isAnd = false) + { + $query = null; + foreach ($relations as $relation) { + $query = (function ($q) use ($relation, $query) { + $q->from($relation->foreignTable); + $q->whereColumn($relation->localKey(true), $relation->foreignKey(false)); + if ($query) $q->whereExists($query); + }); + } + return $query; + } + + + // @[$relationName, $columns] = explode('.', $relationName); + // $relation = $this->getRelation($relationName); + // if (!$relation) return; + // $hasOneWith = $this->withOne[$relationName] ?? null; + // // if ($hasOneWith) + // if ($columns) { + // $this->whereExists(function ($q) use ($relation, $columns) { + // $q->from($relation->foreignTable); + // foreach ($columns as $column) { + // $q->whereColumn( + // $relation->localColumn($column, true), + // $relation->foreignColumn($column, false) + // ); + // } + // }); + // } else { + // $this->whereExists(function ($q) use ($relation) { + // $q->from($relation->foreignTable); + // $q->whereColumn($relation->localKey(true), $relation->foreignKey(false)); + // }); + // } + // } + + + // protected function applyHas( + // bool $isNot, string $relationName, $operator = null, $value = null + // ) { + // @[$relationName, $columns] = explode(':', $relationName); + // $relation = $this->getRelation($relationName); + // if (!$relation) return; + + // $this->select(static::prepareModelColumns($this->columns, $this->model)); + // if ($columns) { + // $columns = explode(',', $columns); + // foreach ($columns as $column) { + // $this->whereNotNull($relation->foreignColumn($column, true)); + // } + // } + // if (func_num_args() > 3) { + // if ($this->isQueryable($operator)) { + // if ($operator instanceof \Closure) { + // call_user_func($operator, $operator = $this->newQuery()); + // } + // foreach ($operator->wheres as $where) { + // if (isset($where['columns'])) + // foreach ($where['columns'] as &$column) { + // $this->addColumnTablePrefix($column, $relationName); + // } + // if (isset($where['column'])) + // $where['column'] = $this->addColumnTablePrefix( + // $where['column'], $relationName + // ); + // $this->pushWhere($where['type'], $where); + // } + // } else { + // $this->prepareValueAndOperator( + // $value, $operator, func_num_args() === 4 + // ); + // $foreignPrimary = $relation->foreignPrimary(true); + // if ($columns) { + // // isHas + // foreach ($columns as $column) { + // $this->where( + // $relation->foreignColumn($column, true), + // $operator, $value + // ); + // } + // } else { + // $this->count($foreignPrimary); + // $this->havingAggregate( + // 'count', $foreignPrimary, $operator, $value + // ); + // if ($value === 0) $isNot = true; + // } + // } + // } + + // $args = $relation->joinArgs(null, false); + + // if ($isNot) { + // if (func_num_args() > 3) { + // $this->leftJoinSub(...$args); + // $this->groupBy($relation->localKey(true)); + // } else { + // $this->leftOuterJoinSub(...$args); + // $this->whereNull($relation->foreignKey()); + // } + // } else { + // $this->joinSub(...$args); + // $this->groupBy($relation->localKey(true)); + // } + // } +} From e446ddee024d536e9ceebe18a6997a46734da6f0 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 11 Dec 2022 23:56:10 +0300 Subject: [PATCH 56/60] Create QueryBuilderRelationsWithTrait.php --- src/Traits/QueryBuilderRelationsWithTrait.php | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/Traits/QueryBuilderRelationsWithTrait.php diff --git a/src/Traits/QueryBuilderRelationsWithTrait.php b/src/Traits/QueryBuilderRelationsWithTrait.php new file mode 100644 index 0000000..deb3d5d --- /dev/null +++ b/src/Traits/QueryBuilderRelationsWithTrait.php @@ -0,0 +1,116 @@ +getRelation($name); + // if ($relation) { + // if ($relation->isOne()) $list = &$this->withOne; + // else $list = &$this->withMany; + // $list[$relation->name] = [$relation, $columns, $query]; + // } + // // echo title('addWith: ' . $name, 3);// . dumpOrm($relation); + // // echo dumpOrm($this->model::$relations); + // return $this; + // } + + // public function old_with(...$props) + // { + // foreach ($props as &$prop) { + // if (is_string($prop)) { + // @[$name, $columns] = explode(':', $prop); + // $this->addWith($name, $columns); + + // } else if (is_array($prop)) { + // foreach ($prop as $name => &$sub) { + // $columns = null; + // $query = null; + + // if (is_string($name)) { + // if (is_string($sub) || is_array($sub)) { + // $columns = $sub; + // } else if (is_callable($sub)) { + // @[$name, $columns] = explode(':', $name); + // $relation = $this->getRelation($name); + // if (!$relation) return; + // $query = new static($this->db, $relation->foreignModel); + // $sub($query); + // } + + // } else if (is_string($sub)) { + // @[$name, $columns] = explode(':', $sub); + // } else { + // throw new \InvalidArgumentException(sprintf( + // 'Incorrect arguments in method %s()', + // __METHOD__ + // )); + // } + // $this->addWith($name, $columns, $query); + // } + // } + // } + // return $this; + // } + + // public function with(...$props) + // { + // $this->withs = array_merge($this->withs, $props); + // echo dumpOrm($this->withs); + // return $this; + // } + + protected function applyWiths(array $ids, array &$models) + { + // // $withOne = array_filter($this->withOne, fn($with) => $with->isOne()); + // if (1 > count($this->withOne)) return; + // // echo title('applyWiths', 3); + // // $columns = static::prepareModelColumns($this->columns, $this->model); + // // $this->select($columns); + // foreach ($this->withOne as [$relation, $columns, $query]) { + // $this->applyWith($relation, $columns, $query); + // } + } + + // protected function applyWith( + // Relation $relation, $columns = null, self $query = null + // ) { + // $columns = static::prepareModelColumns( + // $columns, $relation->foreignModel, $relation->name + // ); + // $this->select($columns); + // // $this->leftJoinSub( + // // $query ?? $relation->foreignTable, + // // $relation->name, + // // "{$relation->name}.{$relation->foreignKey}", + // // $relation->localKey(true) + // // ); + // $this->leftJoinSub(...$relation->joinArgs($query, true)); + // return $this; + // } + + protected function parseWiths(ActiveRecord &$model) + { + if (1 > count($this->withOne)) return; + // echo title('parseWiths', 3) . dumpOrm($model); + $foreigns = []; + foreach ($model->toArray() as $key => $value) { + @[$fname, $fkey] = explode(static::$relationDataSeparator, $key, 2); + if ($fkey && in_array($fname, array_keys($this->withOne))) { + $foreigns[$fname][$fkey] = $value; + unset($model->$key); + } + } + foreach ($foreigns as $fname => $subs) { + // $this->withOne[$fname][0]->addRelated($result, $subs); + $model->addRelatedData($fname, $subs); + } + } +} From e18a181368a2215ebf69c21076c003d331306863 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 11 Dec 2022 23:56:32 +0300 Subject: [PATCH 57/60] Update QueryBuilderRelationsWithTrait.php --- src/Traits/QueryBuilderRelationsWithTrait.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/Traits/QueryBuilderRelationsWithTrait.php b/src/Traits/QueryBuilderRelationsWithTrait.php index deb3d5d..952a884 100644 --- a/src/Traits/QueryBuilderRelationsWithTrait.php +++ b/src/Traits/QueryBuilderRelationsWithTrait.php @@ -60,6 +60,8 @@ trait QueryBuilderRelationsWithTrait // return $this; // } + protected $withs = []; + // public function with(...$props) // { // $this->withs = array_merge($this->withs, $props); @@ -67,6 +69,79 @@ trait QueryBuilderRelationsWithTrait // return $this; // } + public function with(...$props) + { + // echo dumpOrm($props); + $withs = []; + foreach ($props as &$prop) { + if (is_string($prop)) { + $withs[] = $this->levels(explode('.', $prop)); + } else if (is_array($prop)) { + $withs[] = $this->recursiveArrayWith($prop); + } + } + $this->withs = array_merge_recursive(...$withs); + // echo dumpOrm($this->withs); + return $this; + } + + protected function levels($levels, $value = []) + { + $levels = array_reverse($levels); + $with = $value; + foreach ($levels as $level) { + $with = [$level => $with]; + } + return $with; + } + + protected function recursiveArrayWith(array $props) + { + $withs = []; + foreach ($props as $i => $prop) { + $key = is_string($i) ? $i : $prop; + $val = is_string($i) ? $prop : []; + + if (is_array($val)) $val = $this->recursiveArrayWith($val); + else if (is_string($val)) $val = [$val => []]; + + // вложенность связей в ключе + if (is_string($key)) { + $levels = explode('.', $key); + if (count($levels) > 1) { + $key = array_shift($levels); + $val = $this->levels($levels, $val); + } + } + // слияние значений уже существующего ключа + if (isset($withs[$key]) && is_array($withs[$key])) { + if (is_numeric($val)) return; + if (is_string($val)) $val = [$val]; + $val = array_merge($withs[$key], $val); + } + $withs[$key] = $val; + } + return $withs; + } + + // protected function recursiveArrayWith(array $props) + // { + // $withs = []; + // foreach ($props as $i => $prop) { + // $names = is_string($i) ? explode('.', $i) : []; + // if (is_string($prop)) { + // $withs[] = array_merge($names, explode('.', $prop)); + // } else if (is_array($prop)) { + // $subs = $this->recursiveArrayWith($prop); + // foreach ($subs as $sub) { + // $withs[] = array_merge($names, $sub); + // } + // } + // } + // return $withs; + // } + + protected function applyWiths(array $ids, array &$models) { // // $withOne = array_filter($this->withOne, fn($with) => $with->isOne()); @@ -77,6 +152,32 @@ protected function applyWiths(array $ids, array &$models) // foreach ($this->withOne as [$relation, $columns, $query]) { // $this->applyWith($relation, $columns, $query); // } + // return; + if (1 > count($this->withs)) return; + $keys = array_keys($this->withs); + foreach ($keys as $key) { + $this->applyWith($key, $this->withs[$key], $ids, $models); + } + } + + protected function applyWith($key, $val, array $ids, array &$models) + { + $relation = $this->getRelation($key); + $qb = (new static($this->db, $relation->foreignModel)); + $qb->whereIn($relation->foreignKey, $ids); + if (!empty($val)) $qb->with($val); + $subModels = $qb->get(); + if (!$subModels) return; + $idsLocal = []; + foreach ($subModels as $subModel) { + $idsLocal[] = $subModel->primaryValue(); + foreach ($models as $model) { + if ($model->{$relation->localKey} == $subModel->{$relation->foreignKey}) { + $model->addRelated($relation->name, $subModel); + break; + } + } + } } // protected function applyWith( From 08f41eea34cf569d0cafe759431727c1864644ae Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 11 Dec 2022 23:56:37 +0300 Subject: [PATCH 58/60] Update QueryBuilder.php --- src/QueryBuilder.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 86ced68..c13bf42 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -64,14 +64,19 @@ protected function primaryKey(): string */ public function get($columns = null): array { - $this->applyWiths(); + $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->withs)) $this->parseWiths($model); + if (0 < count($this->withOne)) $this->parseWiths($model); + $ids[] = $model->primaryValue(); } - $models = array_unique($models); + // $models = array_unique($models); + $this->applyRelationsAfter($ids, $models); return $models; } @@ -80,14 +85,15 @@ public function get($columns = null): array * @param array|null столбцы для получения * @return array|null найденная запись */ - public function one($columns = null) + public function one($columns = null) { - $this->applyWiths(); + $this->applyRelationsBefore(); if ($columns) $this->select(...func_get_args()); $model = $this->limit(1)->query()->object($this->model); if ($model) { - if (0 < count($this->withs)) $this->parseWiths($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(); From 33e60c53628c88e99014aef1b74f4def6167e86b Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Sun, 11 Dec 2022 23:59:51 +0300 Subject: [PATCH 59/60] Update QueryBuilderRelationsWithTrait.php --- src/Traits/QueryBuilderRelationsWithTrait.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Traits/QueryBuilderRelationsWithTrait.php b/src/Traits/QueryBuilderRelationsWithTrait.php index 952a884..bb92852 100644 --- a/src/Traits/QueryBuilderRelationsWithTrait.php +++ b/src/Traits/QueryBuilderRelationsWithTrait.php @@ -163,14 +163,16 @@ protected function applyWiths(array $ids, array &$models) protected function applyWith($key, $val, array $ids, array &$models) { $relation = $this->getRelation($key); + $qb = (new static($this->db, $relation->foreignModel)); $qb->whereIn($relation->foreignKey, $ids); if (!empty($val)) $qb->with($val); $subModels = $qb->get(); if (!$subModels) return; - $idsLocal = []; + + // $idsLocal = []; foreach ($subModels as $subModel) { - $idsLocal[] = $subModel->primaryValue(); + // $idsLocal[] = $subModel->primaryValue(); foreach ($models as $model) { if ($model->{$relation->localKey} == $subModel->{$relation->foreignKey}) { $model->addRelated($relation->name, $subModel); From 3f08ff2744dac28b9ed86c69e09305e44bb6bac0 Mon Sep 17 00:00:00 2001 From: Egor Vasyakin Date: Mon, 31 Jul 2023 00:54:43 +0300 Subject: [PATCH 60/60] some temporary changes --- src/Traits/ActiveRecordRelatedsTrait.php | 4 +- src/Traits/QueryBuilderRelationsHasTrait.php | 157 +++++++++++++++--- src/Traits/QueryBuilderRelationsTrait.php | 2 +- src/Traits/QueryBuilderRelationsWithTrait.php | 7 +- 4 files changed, 145 insertions(+), 25 deletions(-) diff --git a/src/Traits/ActiveRecordRelatedsTrait.php b/src/Traits/ActiveRecordRelatedsTrait.php index d1f26ce..dcc99d7 100644 --- a/src/Traits/ActiveRecordRelatedsTrait.php +++ b/src/Traits/ActiveRecordRelatedsTrait.php @@ -40,8 +40,8 @@ public function addRelated(string $name, ActiveRecord $related) $relation = static::getRelation($name); $related = $related->identityMapSave(); if (!$relation) return $this; - if (false) { - // if ($relation->isOne()) { + // if (false) { + if ($relation->isOne()) { if (!isset($this->relatedCollections[$name]) && $related->primaryValue()) { $this->relatedCollections[$name] = &$related; } diff --git a/src/Traits/QueryBuilderRelationsHasTrait.php b/src/Traits/QueryBuilderRelationsHasTrait.php index 0e7e385..c17955c 100644 --- a/src/Traits/QueryBuilderRelationsHasTrait.php +++ b/src/Traits/QueryBuilderRelationsHasTrait.php @@ -51,32 +51,95 @@ protected function applyHas( ) { $relationNames = explode('.', $relationName); $model = $this->model; + // $relations = []; + // foreach ($relationNames as $name) { + // $relation = $model::getRelation($name); + // $model = $relation->foreignModel; + // $relations[] = $relation; + // } + // if (!count($relations)) return; + + $relationGroups = []; + $dbName = null; $relations = []; foreach ($relationNames as $name) { $relation = $model::getRelation($name); $model = $relation->foreignModel; + + if ($dbName !== $model::dbName()) { + if (!empty($relations)) { + $relationGroups[] = $relations; + } + $dbName = $model::dbName(); + $relations = []; + } + $relations[] = $relation; } - if (!count($relations)) return; + if (!empty($relations)) { + $relationGroups[] = $relations; + } + if (!count($relationGroups)) return; + // foreach ($relationGroups as $relations) { + // foreach ($relations as $relation) { + // echo dumpOrm($relation); + // } + // echo '
'; + // } + // exit(); + - if ($hasCount = func_num_args() > 3) { + if (func_num_args() > 3) { $this->prepareValueAndOperator( $value, $operator, func_num_args() === 4 ); + $opval = [$operator, $value]; + } else { + $opval = null; } - $relations = array_reverse($relations); - $query = $this->realApplyHas($relations, $operator, $value, $hasCount); + + // exit(); + // $relations = array_reverse($relations); + // $query = $this->realApplyHas($relations, $operator, $value, $isCount); - if (count($relations) < 2 && $hasCount) { - $this->where($query, $operator, $value); - echo dumpOrm($this->getSql()); - echo dumpOrm($this->getBindings()); + $last = count($relationGroups) - 1; + if (0 === $last) { + $relations = array_reverse($relationGroups[0]); + $query = $this->realApplyHas($relations, $opval); + + if (count($relations) < 2 && $opval) { + $this->where($query, ...$opval); + echo dumpOrm($this->getSql()); + echo dumpOrm($this->getBindings()); + } else { + $this->whereExists($query); + } } else { - $this->whereExists($query); + $ids = null; + foreach ($relationGroups as $i => $relations) { + $ids = $this->applyGroupHas( + $relations, $ids, $last === $i ? $opval : null + ); + // $relations = array_reverse($relations); + // $query = $this->realApplyHas($relations, $last === $i ? $opval : null); + // var_dump($query); + // echo '
'; + } + $this->whereIn($this->primaryKey(), $ids); } + + // if (count($relations) < 2 && $opval) { + // $this->where($query, ...$opval); + // echo dumpOrm($this->getSql()); + // echo dumpOrm($this->getBindings()); + // } else { + // $this->whereExists($query); + // } + + // if (in_array($operator, ['<', '<='])) { // $query = $this->realApplyNotHas($relations, $operator); // $this->orWhereNotExists($query); @@ -86,22 +149,55 @@ protected function applyHas( // $this->whereRaw('EXISTS (SELECT COUNT(`id`) AS `count_id` FROM `company` WHERE `user`.`id` = `company`.`user_id` AND (SELECT COUNT(`id`) AS `count_id` FROM `user` WHERE `company`.`user_id` = `user`.`id`) > 100)'); } - protected function realApplyHas($relations, $operator, $value, bool $hasCount = false) + protected function applyGroupHas($relations, array $ids = null, array $opval = null) + { + echo dumpOrm($relations); + $relations = array_reverse($relations); + $last = count($relations) - 1; + $q = clone $this; + $q->db = $relations[0]->foreignModel::db(); + $q->withs = []; + $q->has = []; + $q->resetFrom($relations[0]->localModel::tableName()); + if ($ids) { + $q->whereIn($relations[0]->localKey(true), $ids); + $ids = null; + // $q->resetSelect(['iid' => $relations[0]->foreignKey(true)]); + array_pop($relations); + } else { + // + } + $query = $q->realApplyHas($relations, $opval); + if (count($relations) < 2 && $opval) { + $q->where($query, ...$opval); + } else { + $q->whereExists($query); + } + echo '

SQL:

'; + echo dumpOrm($q->getSql()); + echo dumpOrm($q->getBindings()); + echo '
'; + $models = $q->get(); + echo dumpOrm($models); + $ids = []; + foreach ($models as $model) { + $ids[] = $model->id; + } + echo dumpOrm($ids); + return $ids; + } + + protected function realApplyHas($relations, array $opval = null) { $query = null; foreach ($relations as $i => $relation) { - $query = function ($q) use ( - $relation, $query, $value, $operator, $hasCount, $i - ) { + $query = function ($q) use ($relation, $query, $opval, $i) { $q->from($relation->foreignTable); $q->whereColumn($relation->localKey(true), $relation->foreignKey(false)); if ($query) { - if (1 == $i && $hasCount) { - $q->where($query, $operator, $value); - } else { - $q->whereExists($query); - } - } else if (0 == $i && $hasCount) { + if (1 == $i && $opval) $q->where($query, ...$opval); + else $q->whereExists($query); + } else if (0 == $i && $opval) { $q->count('id'); } }; @@ -109,6 +205,29 @@ protected function realApplyHas($relations, $operator, $value, bool $hasCount = return $query; } + // protected function realApplyHas($relations, $operator, $value, bool $isCount = false) + // { + // $query = null; + // foreach ($relations as $i => $relation) { + // $query = function ($q) use ( + // $relation, $query, $value, $operator, $isCount, $i + // ) { + // $q->from($relation->foreignTable); + // $q->whereColumn($relation->localKey(true), $relation->foreignKey(false)); + // if ($query) { + // if (1 == $i && $isCount) { + // $q->where($query, $operator, $value); + // } else { + // $q->whereExists($query); + // } + // } else if (0 == $i && $isCount) { + // $q->count('id'); + // } + // }; + // } + // return $query; + // } + protected function realApplyNotHas($relations, $operator, bool $isAnd = false) { $query = null; diff --git a/src/Traits/QueryBuilderRelationsTrait.php b/src/Traits/QueryBuilderRelationsTrait.php index 2f044a9..07f866f 100644 --- a/src/Traits/QueryBuilderRelationsTrait.php +++ b/src/Traits/QueryBuilderRelationsTrait.php @@ -81,7 +81,7 @@ protected function applyRelationsBefore() // $columns = static::prepareModelColumns($this->columns, $this->model); // $this->select($columns); // $this->applyWiths(); - // $this->applyHases(); + $this->applyHases(); } protected function applyRelationsAfter(array $ids, array &$models) diff --git a/src/Traits/QueryBuilderRelationsWithTrait.php b/src/Traits/QueryBuilderRelationsWithTrait.php index bb92852..e381a6f 100644 --- a/src/Traits/QueryBuilderRelationsWithTrait.php +++ b/src/Traits/QueryBuilderRelationsWithTrait.php @@ -163,8 +163,9 @@ protected function applyWiths(array $ids, array &$models) protected function applyWith($key, $val, array $ids, array &$models) { $relation = $this->getRelation($key); - - $qb = (new static($this->db, $relation->foreignModel)); + $fModel = $relation->foreignModel; + $db = $fModel::db(); + $qb = (new static($db, $fModel)); $qb->whereIn($relation->foreignKey, $ids); if (!empty($val)) $qb->with($val); $subModels = $qb->get(); @@ -176,7 +177,7 @@ protected function applyWith($key, $val, array $ids, array &$models) foreach ($models as $model) { if ($model->{$relation->localKey} == $subModel->{$relation->foreignKey}) { $model->addRelated($relation->name, $subModel); - break; + // break; } } }