From f2a9fc473ec6946fbbcc256f13cdab238c82f779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mr=C2=B7x?= Date: Wed, 11 Aug 2021 15:03:28 +0800 Subject: [PATCH] e --- .gitignore | 34 + .phpstorm.meta.php | 15 + composer.json | 26 + src/ActiveQuery.php | 310 +++++++ src/ActiveRecord.php | 420 +++++++++ src/Base/AbstractCollection.php | 161 ++++ src/Base/BaseActiveRecord.php | 1074 ++++++++++++++++++++++++ src/Base/CollectionIterator.php | 75 ++ src/Base/ConditionClassMap.php | 71 ++ src/Collection.php | 242 ++++++ src/Command.php | 318 +++++++ src/Condition/BetweenCondition.php | 23 + src/Condition/ChildCondition.php | 22 + src/Condition/Condition.php | 83 ++ src/Condition/DefaultCondition.php | 24 + src/Condition/HashCondition.php | 31 + src/Condition/InCondition.php | 31 + src/Condition/JsonCondition.php | 20 + src/Condition/LLikeCondition.php | 28 + src/Condition/LikeCondition.php | 28 + src/Condition/MathematicsCondition.php | 78 ++ src/Condition/NotBetweenCondition.php | 22 + src/Condition/NotInCondition.php | 28 + src/Condition/NotLikeCondition.php | 28 + src/Condition/OrCondition.php | 27 + src/Condition/RLikeCondition.php | 28 + src/Connection.php | 341 ++++++++ src/DatabasesProviders.php | 116 +++ src/Db.php | 365 ++++++++ src/HasCount.php | 39 + src/HasMany.php | 44 + src/HasOne.php | 45 + src/IOrm.php | 50 ++ src/ISqlBuilder.php | 10 + src/Mysql/Columns.php | 406 +++++++++ src/Mysql/Schema.php | 36 + src/Orm/Condition.php | 29 + src/Pagination.php | 200 +++++ src/Query.php | 54 ++ src/Relation.php | 111 +++ src/SqlBuilder.php | 369 ++++++++ src/Traits/Builder.php | 218 +++++ src/Traits/HasBase.php | 83 ++ src/Traits/QueryTrait.php | 938 +++++++++++++++++++++ src/Traits/When.php | 77 ++ 45 files changed, 6778 insertions(+) create mode 100644 .gitignore create mode 100644 .phpstorm.meta.php create mode 100644 composer.json create mode 100644 src/ActiveQuery.php create mode 100644 src/ActiveRecord.php create mode 100644 src/Base/AbstractCollection.php create mode 100644 src/Base/BaseActiveRecord.php create mode 100644 src/Base/CollectionIterator.php create mode 100644 src/Base/ConditionClassMap.php create mode 100644 src/Collection.php create mode 100644 src/Command.php create mode 100644 src/Condition/BetweenCondition.php create mode 100644 src/Condition/ChildCondition.php create mode 100644 src/Condition/Condition.php create mode 100644 src/Condition/DefaultCondition.php create mode 100644 src/Condition/HashCondition.php create mode 100644 src/Condition/InCondition.php create mode 100644 src/Condition/JsonCondition.php create mode 100644 src/Condition/LLikeCondition.php create mode 100644 src/Condition/LikeCondition.php create mode 100644 src/Condition/MathematicsCondition.php create mode 100644 src/Condition/NotBetweenCondition.php create mode 100644 src/Condition/NotInCondition.php create mode 100644 src/Condition/NotLikeCondition.php create mode 100644 src/Condition/OrCondition.php create mode 100644 src/Condition/RLikeCondition.php create mode 100644 src/Connection.php create mode 100644 src/DatabasesProviders.php create mode 100644 src/Db.php create mode 100644 src/HasCount.php create mode 100644 src/HasMany.php create mode 100644 src/HasOne.php create mode 100644 src/IOrm.php create mode 100644 src/ISqlBuilder.php create mode 100644 src/Mysql/Columns.php create mode 100644 src/Mysql/Schema.php create mode 100644 src/Orm/Condition.php create mode 100644 src/Pagination.php create mode 100644 src/Query.php create mode 100644 src/Relation.php create mode 100644 src/SqlBuilder.php create mode 100644 src/Traits/Builder.php create mode 100644 src/Traits/HasBase.php create mode 100644 src/Traits/QueryTrait.php create mode 100644 src/Traits/When.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efca283 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Created by .ignore support plugin (hsz.mobi) +### Yii template +assets/* +!assets/.gitignore +protected/runtime/* +!protected/runtime/.gitignore +protected/data/*.db +themes/classic/views/ + +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen + +composer.lock + +*.log +commands/result +config/setting.php +tests/ +vendor/ +runtime/ + +*.xml +*.lock + +oot +d + +composer.lock diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php new file mode 100644 index 0000000..2064435 --- /dev/null +++ b/.phpstorm.meta.php @@ -0,0 +1,15 @@ +=8.0", + "ext-json": "*", + "ext-pdo": "*", + "game-worker/snowflake": "dev-master" + }, + "autoload": { + "psr-4": { + "Database\\": "src/" + } + }, + "require-dev": { + "kwn/php-rdkafka-stubs": "^2.0" + } +} + diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php new file mode 100644 index 0000000..5de5566 --- /dev/null +++ b/src/ActiveQuery.php @@ -0,0 +1,310 @@ +modelClass = $model; + + $this->builder = SqlBuilder::builder($this); + parent::__construct($config); + } + + + /** + * 清除不完整数据 + */ + public function clear() + { + $this->db = null; + $this->useCache = false; + $this->with = []; + } + + /** + * @param $key + * @param $value + * @return $this + */ + public function addParam($key, $value): static + { + $this->attributes[$key] = $value; + return $this; + } + + /** + * @param array $values + * @return $this + */ + public function addParams(array $values): static + { + foreach ($values as $key => $val) { + $this->addParam($key, $val); + } + return $this; + } + + /** + * @param $name + * @return $this + */ + public function with($name): static + { + if (empty($name)) { + return $this; + } + if (is_string($name)) { + $name = explode(',', $name); + } + foreach ($name as $val) { + array_push($this->with, $val); + } + return $this; + } + + + /** + * @param $sql + * @param array $params + * @return mixed + * @throws Exception + */ + public function execute($sql, array $params = []): Command + { + return $this->modelClass::getDb()->createCommand($sql, $params); + } + + + /** + * @return ActiveRecord|null + * @throws Exception + */ + public function first(): ActiveRecord|null + { + $data = $this->execute($this->builder->one())->one(); + if (empty($data)) { + return NULL; + } + return $this->modelClass::populate($data); + } + + + /** + * @return string + * @throws Exception + */ + public function toSql(): string + { + return $this->builder->get(); + } + + + /** + * @return array|Collection + */ + public function get(): Collection|array + { + return $this->all(); + } + + + /** + * @throws Exception + */ + public function flush(): array|bool|int|string|null + { + return $this->execute($this->builder->truncate())->exec(); + } + + + /** + * @param int $size + * @param callable $callback + * @return Pagination + * @throws Exception + */ + public function page(int $size, callable $callback): Pagination + { + $pagination = new Pagination($this); + $pagination->setOffset(0); + $pagination->setLimit($size); + $pagination->setCallback($callback); + return $pagination; + } + + /** + * @param string $field + * @param string $setKey + * + * @return array|null + * @throws Exception + */ + public function column(string $field, string $setKey = ''): ?array + { + return $this->all()->column($field, $setKey); + } + + /** + * @return array|Collection + * @throws + */ + public function all(): Collection|array + { + $data = $this->execute($this->builder->all())->all(); + + $collect = new Collection($this, $data, $this->modelClass); + if ($this->asArray) { + return $collect->toArray(); + } + return $collect; + } + + /** + * @param ActiveRecord $model + * @param $data + * @return ActiveRecord + * @throws Exception + */ + public function populate(ActiveRecord $model, $data): ActiveRecord + { + return $this->getWith($model::populate($data)); + } + + + /** + * @param ActiveRecord $model + * @return ActiveRecord + */ + public function getWith(ActiveRecord $model): ActiveRecord + { + if (empty($this->with) || !is_array($this->with)) { + return $model; + } + return $model->setWith($this->with); + } + + /** + * @return int + * @throws Exception + */ + public function count(): int + { + $this->select = ['COUNT(*)']; + $data = $this->execute($this->builder->count())->one(); + if ($data && is_array($data)) { + return (int)array_shift($data); + } + return 0; + } + + + /** + * @param array $data + * @return array|Command|bool|int|string + * @throws Exception + */ + public function batchUpdate(array $data): Command|array|bool|int|string + { + $generate = $this->builder->update($data); + if (is_bool($generate)) { + return $generate; + } + return $this->execute(...$generate)->exec(); + } + + /** + * @param array $data + * @return bool + * @throws Exception + */ + public function batchInsert(array $data): bool + { + [$sql, $params] = $this->builder->insert($data, true); + + + return $this->execute($sql, $params)->exec(); + } + + /** + * @param $filed + * + * @return null + * @throws Exception + */ + public function value($filed) + { + return $this->first()[$filed] ?? null; + } + + /** + * @return bool + * @throws Exception + */ + public function exists(): bool + { + return !empty($this->execute($this->builder->one())->fetchColumn()); + } + + + /** + * @param bool $getSql + * @return string|bool + * @throws Exception + */ + public function delete(bool $getSql = false): string|bool + { + $sql = $this->builder->delete(); + if ($getSql === false) { + return $this->execute($sql)->delete(); + } + return $sql; + } +} diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php new file mode 100644 index 0000000..a75e082 --- /dev/null +++ b/src/ActiveRecord.php @@ -0,0 +1,420 @@ +mathematics([$column => $value], '+', null)) { + return false; + } + $this->{$column} += $value; + return $this->refresh(); + } + + + /** + * @param string $column + * @param int $value + * @return ActiveRecord|false + * @throws Exception + */ + public function decrement(string $column, int $value): bool|ActiveRecord + { + if (!$this->mathematics([$column => $value], '-', null)) { + return false; + } + $this->{$column} -= $value; + return $this->refresh(); + } + + + /** + * @param array $columns + * @return ActiveRecord|false + * @throws Exception + */ + public function increments(array $columns): bool|static + { + if (!$this->mathematics($columns, '+', null)) { + return false; + } + foreach ($columns as $key => $attribute) { + $this->$key += $attribute; + } + return $this; + } + + + /** + * @param array $columns + * @return ActiveRecord|false + * @throws Exception + */ + public function decrements(array $columns): bool|static + { + if (!$this->mathematics($columns, '-', null)) { + return false; + } + foreach ($columns as $key => $attribute) { + $this->$key -= $attribute; + } + return $this; + } + + /** + * @param array $condition + * @param array $attributes + * @return bool|ActiveRecord + * @throws ReflectionException + * @throws NotFindClassException + * @throws Exception + */ + public static function findOrCreate(array $condition, array $attributes = []): bool|static + { + $logger = Kiri::app()->getLogger(); + + /** @var static $select */ + $select = static::find()->where($condition)->first(); + if (!empty($select)) { + return $select; + } + if (empty($attributes)) { + return $logger->addError(FIND_OR_CREATE_MESSAGE, 'mysql'); + } + $select = duplicate(static::class); + $select->attributes = $attributes; + if (!$select->save()) { + return $logger->addError($select->getLastError(), 'mysql'); + } + return $select; + } + + + /** + * @param array $condition + * @param array $attributes + * @return bool|static + * @throws Exception + */ + public static function createOrUpdate(array $condition, array $attributes = []): bool|static + { + $logger = Kiri::app()->getLogger(); + if (empty($attributes)) { + return $logger->addError(FIND_OR_CREATE_MESSAGE, 'mysql'); + } + /** @var static $select */ + $select = static::find()->where($condition)->first(); + if (empty($select)) { + $select = duplicate(static::class); + } + $select->attributes = $attributes; + if (!$select->save()) { + return $logger->addError($select->getLastError(), 'mysql'); + } + return $select; + } + + + /** + * @param $action + * @param $columns + * @param null|array $condition + * @return array|bool|int|string|null + * @throws Exception + */ + private function mathematics($columns, $action, ?array $condition): int|bool|array|string|null + { + if (empty($condition)) { + $condition = [$this->getPrimary() => $this->getPrimaryValue()]; + } + + $activeQuery = static::find()->where($condition); + $create = SqlBuilder::builder($activeQuery)->mathematics($columns, $action); + if (is_bool($create)) { + return false; + } + return static::getDb()->createCommand($create[0], $create[1])->exec(); + } + + + /** + * @param array $fields + * @return ActiveRecord|bool + * @throws Exception + */ + public function update(array $fields): static|bool + { + return $this->save($fields); + } + + + /** + * @param array $data + * @return bool + * @throws Exception + */ + public static function inserts(array $data): bool + { + /** @var static $class */ + $class = Kiri::createObject(['class' => static::class]); + if (empty($data)) { + return $class->addError('Insert data empty.', 'mysql'); + } + return $class::find()->batchInsert($data); + } + + /** + * @return bool + * @throws Exception + */ + public function delete(): bool + { + $conditions = $this->_oldAttributes; + if (empty($conditions)) { + return $this->addError("Delete condition do not empty.", 'mysql'); + } + $primary = $this->getPrimary(); + + if (!empty($primary)) { + $conditions = [$primary => $this->getAttribute($primary)]; + } + return static::deleteByCondition($conditions); + } + + + /** + * @param mixed $condition + * @param array $attributes + * + * @return bool + * @throws NotFindClassException + * @throws ReflectionException + * @throws Exception + */ + public static function updateAll(mixed $condition, array $attributes = []): bool + { + $condition = static::find()->where($condition); + return $condition->batchUpdate($attributes); + } + + /** + * @param $condition + * @param array $attributes + * + * @return array|Collection + * @throws Exception + */ + public static function findAll($condition, array $attributes = []): array|Collection + { + $query = static::find()->where($condition); + if (!empty($attributes)) { + $query->bindParams($attributes); + } + return $query->all(); + } + + /** + * @param $method + * @return mixed + * @throws Exception + */ + private function resolveObject($method): mixed + { + $resolve = $this->{$this->getRelate($method)}(); + if ($resolve instanceof HasBase) { + $resolve = $resolve->get(); + } + if ($resolve instanceof Collection) { + return $resolve->toArray(); + } else if ($resolve instanceof ActiveRecord) { + return $resolve->toArray(); + } else if (is_object($resolve)) { + return get_object_vars($resolve); + } else { + return $resolve; + } + } + + + /** + * @return array + * @throws Exception + */ + public function toArray(): array + { + $data = $this->_attributes; + + $lists = Kiri::getAnnotation()->getGets(static::class); + foreach ($lists as $key => $item) { + $data[$key] = $this->{$item}($data[$key] ?? null); + } + return array_merge($data, $this->runRelate()); + } + + /** + * @return array + * @throws Exception + */ + private function runRelate(): array + { + $relates = []; + if (empty($with = $this->getWith())) { + return $relates; + } + foreach ($with as $val) { + $relates[$val] = $this->resolveObject($val); + } + return $relates; + } + + + /** + * @param string $modelName + * @param $foreignKey + * @param $localKey + * @return HasOne|ActiveQuery + * @throws Exception + */ + public function hasOne(string $modelName, $foreignKey, $localKey): HasOne|ActiveQuery + { + if (($value = $this->getAttribute($localKey)) === null) { + throw new Exception("Need join table primary key."); + } + + $relation = $this->getRelation(); + + return new HasOne($modelName, $foreignKey, $value, $relation); + } + + + /** + * @param $modelName + * @param $foreignKey + * @param $localKey + * @return ActiveQuery|HasCount + * @throws Exception + */ + public function hasCount($modelName, $foreignKey, $localKey): ActiveQuery|HasCount + { + if (($value = $this->getAttribute($localKey)) === null) { + throw new Exception("Need join table primary key."); + } + + $relation = $this->getRelation(); + + return new HasCount($modelName, $foreignKey, $value, $relation); + } + + + /** + * @param $modelName + * @param $foreignKey + * @param $localKey + * @return ActiveQuery|HasMany + * @throws Exception + */ + public function hasMany($modelName, $foreignKey, $localKey): ActiveQuery|HasMany + { + if (($value = $this->getAttribute($localKey)) === null) { + throw new Exception("Need join table primary key."); + } + + $relation = $this->getRelation(); + + return new HasMany($modelName, $foreignKey, $value, $relation); + } + + /** + * @param $modelName + * @param $foreignKey + * @param $localKey + * @return ActiveQuery|HasMany + * @throws Exception + */ + public function hasIn($modelName, $foreignKey, $localKey): ActiveQuery|HasMany + { + if (($value = $this->getAttribute($localKey)) === null) { + throw new Exception("Need join table primary key."); + } + + $relation = $this->getRelation(); + + return new HasMany($modelName, $foreignKey, $value, $relation); + } + + /** + * @return bool + * @throws Exception + */ + public function afterDelete(): bool + { + if (!$this->hasPrimary()) { + return TRUE; + } + $value = $this->getPrimaryValue(); + if (empty($value)) { + return TRUE; + } + return TRUE; + } + + /** + * @return bool + * @throws Exception + */ + public function beforeDelete(): bool + { + if (!$this->hasPrimary()) { + return TRUE; + } + $value = $this->getPrimaryValue(); + if (empty($value)) { + return TRUE; + } + return TRUE; + } +} diff --git a/src/Base/AbstractCollection.php b/src/Base/AbstractCollection.php new file mode 100644 index 0000000..7f4e7f7 --- /dev/null +++ b/src/Base/AbstractCollection.php @@ -0,0 +1,161 @@ +query, $this->model, $this->_item); + } + + + /** + * Collection constructor. + * + * @param $query + * @param array $array + * @param string|ActiveRecord|null $model + * @throws Exception + */ + public function __construct($query, array $array = [], string|ActiveRecord $model = null) + { + $this->_item = $array; + $this->query = $query; + $this->model = duplicate($model); + + parent::__construct([]); + } + + + /** + * @return int + */ + #[Pure] public function getLength(): int + { + return count($this->_item); + } + + + /** + * @param $item + */ + public function setItems($item) + { + $this->_item = $item; + } + + + /** + * @param $model + */ + public function setModel($model) + { + $this->model = $model; + } + + /** + * @param $item + */ + public function addItem($item) + { + array_push($this->_item, $item); + } + + /** + * @return Traversable|CollectionIterator|ArrayIterator + * @throws Exception + */ + public function getIterator(): Traversable|CollectionIterator|ArrayIterator + { + return new CollectionIterator($this->model, $this->query, $this->_item); + } + + + /** + * @return mixed + * @throws Exception + */ + public function getModel(): ActiveRecord + { + return $this->model; + } + + + /** + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return !empty($this->_item) && isset($this->_item[$offset]); + } + + /** + * @param mixed $offset + * @return ActiveRecord|null + * @throws Exception + */ + public function offsetGet(mixed $offset): ?ActiveRecord + { + if (!$this->offsetExists($offset)) { + return NULL; + } + if (!($this->_item[$offset] instanceof ActiveRecord)) { + return $this->model->setAttributes($this->_item[$offset]); + } + return $this->_item[$offset]; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet(mixed $offset, mixed $value) + { + $this->_item[$offset] = $value; + } + + + /** + * @param mixed $offset + */ + public function offsetUnset(mixed $offset) + { + if ($this->offsetExists($offset)) { + unset($this->_item[$offset]); + } + } +} diff --git a/src/Base/BaseActiveRecord.php b/src/Base/BaseActiveRecord.php new file mode 100644 index 0000000..7d5cde3 --- /dev/null +++ b/src/Base/BaseActiveRecord.php @@ -0,0 +1,1074 @@ +get(EventDispatch::class); + } + + + /** + * @param $data + * @return ActiveRecord + */ + public function setWith($data): static + { + if (empty($data)) { + return $this; + } + $this->_with = $data; + return $this; + } + + + /** + * @return array|null + */ + public function getWith(): array|null + { + return $this->_with; + } + + + /** + * object init + */ + public function clean() + { + $this->_attributes = []; + $this->_oldAttributes = []; + } + + + /** + * @param Relation $relation + */ + public function setRelation(Relation $relation) + { + $this->_relation = $relation; + } + + + /** + * @throws Exception + */ + public function init() + { + $an = Kiri::app()->getAnnotation(); + $an->injectProperty($this); + } + + + /** + * @return array + */ + public function getActions(): array + { + return $this->actions; + } + + + /** + * @return bool + */ + public function getIsCreate(): bool + { + return $this->isNewExample === TRUE; + } + + + /** + * @param bool $bool + * @return $this + */ + public function setIsCreate(bool $bool = FALSE): static + { + $this->isNewExample = $bool; + return $this; + } + + /** + * @return mixed + * @throws Exception + * get last exception or other error + */ + public function getLastError(): mixed + { + return Kiri::app()->getLogger()->getLastError('mysql'); + } + + + /** + * @return bool + * @throws Exception + */ + public function hasPrimary(): bool + { + if ($this->primary !== NULL) { + return true; + } + $primary = static::getColumns()->getPrimaryKeys(); + if (!empty($primary)) { + return $this->primary = is_array($primary) ? current($primary) : $primary; + } + return false; + } + + + /** + * @throws Exception + */ + public function isAutoIncrement(): bool + { + return $this->getAutoIncrement() !== null; + } + + /** + * @throws Exception + */ + public function getAutoIncrement(): int|string|null + { + return static::getColumns()->getAutoIncrement(); + } + + /** + * @return null|string + * @throws Exception + */ + public function getPrimary(): ?string + { + if (!$this->hasPrimary()) { + return null; + } + return $this->primary; + } + + /** + * @return int|null + * @throws Exception + */ + public function getPrimaryValue(): ?int + { + if (!$this->hasPrimary()) { + return null; + } + return $this->getAttribute($this->primary); + } + + /** + * @param $param + * @param null $db + * @return BaseActiveRecord|null + * @throws NotFindClassException + * @throws ReflectionException + * @throws Exception + */ + public static function findOne($param, $db = NULL): static|null + { + if (is_bool($param)) { + return null; + } + if (is_numeric($param)) { + $param = static::getPrimaryCondition($param); + } + return static::find()->where($param)->first(); + } + + + /** + * @param $param + * @return array + * @throws Exception + */ + private static function getPrimaryCondition($param): array + { + $primary = static::getColumns()->getPrimaryKeys(); + if (empty($primary)) { + throw new Exception('Primary key cannot be empty.'); + } + if (is_array($primary)) { + $primary = current($primary); + } + return [$primary => $param]; + } + + + /** + * @param null $field + * @return ActiveRecord|null + * @throws Exception + * @throws Exception + */ + public static function max($field = null): ?ActiveRecord + { + $columns = static::getColumns(); + if (empty($field)) { + $field = $columns->getFirstPrimary(); + } + $columns = $columns->get_fields(); + if (!isset($columns[$field])) { + return null; + } + $first = static::find()->max($field)->first(); + if (empty($first)) { + return null; + } + return $first[$field]; + } + + + /** + * @return ActiveQuery + */ + public static function find(): ActiveQuery + { + return static::query(); + } + + + /** + * @return ActiveQuery + */ + public static function query(): ActiveQuery + { + return new ActiveQuery(get_called_class()); + } + + + /** + * @throws ConfigException + */ + protected function getConnection() + { + return Config::get('connections.' . static::$connection, null, true); + } + + + /** + * @param null $condition + * @param array $attributes + * + * @param bool $if_condition_is_null + * @return bool + * @throws Exception + */ + public static function deleteByCondition($condition = NULL, array $attributes = [], bool $if_condition_is_null = false): bool + { + if (empty($condition)) { + if (!$if_condition_is_null) { + return false; + } + return static::find()->delete(); + } + $model = static::find()->ifNotWhere($if_condition_is_null)->where($condition); + if (!empty($attributes)) { + $model->bindParams($attributes); + } + return $model->delete(); + } + + + /** + * @return array + * @throws Exception + */ + public function getAttributes(): array + { + return $this->toArray(); + } + + /** + * @return array + */ + public function getOldAttributes(): array + { + return $this->_oldAttributes; + } + + /** + * @param $name + * @param $value + * @return mixed + */ + public function setAttribute($name, $value): mixed + { + return $this->_attributes[$name] = $value; + } + + /** + * @param $name + * @param $value + * @return mixed + */ + public function setOldAttribute($name, $value): mixed + { + return $this->_oldAttributes[$name] = $value; + } + + /** + * @param array $param + * @return $this + * @throws Exception + */ + public function setAttributes(array $param): static + { + if (empty($param)) { + return $this; + } + $this->_attributes = array_merge($this->_attributes, $param); + return $this; + } + + /** + * @param $param + * @return $this + */ + public function setOldAttributes($param): static + { + if (empty($param) || !is_array($param)) { + return $this; + } + foreach ($param as $key => $val) { + $this->setOldAttribute($key, $val); + } + return $this; + } + + /** + * @param $attributes + * @param $param + * @return $this|bool + * @throws Exception + */ + private function insert($param, $attributes): bool|static + { + [$sql, $param] = SqlBuilder::builder(static::find())->insert($param); + $dbConnection = static::getDb()->createCommand($sql, $param); + if (!($lastId = (int)$dbConnection->save(true, $this))) { + throw new Exception('保存失败.'); + } + $lastId = $this->setPrimary($lastId, $param); + + $this->refresh()->afterSave($attributes, $param); + + return $lastId; + } + + + /** + * @param $lastId + * @param $param + * @return static + * @throws Exception + */ + private function setPrimary($lastId, $param): static + { + if ($this->isAutoIncrement()) { + $this->setAttribute($this->getAutoIncrement(), (int)$lastId); + return $this; + } + + if (!$this->hasPrimary()) { + return $this; + } + + $primary = $this->getPrimary(); + if (!isset($param[$primary]) || empty($param[$primary])) { + $this->setAttribute($primary, (int)$lastId); + } + return $this->setAttributes($param); + } + + + /** + * @param $fields + * @param $condition + * @param $param + * @return $this|bool + * @throws Exception + */ + private function updateInternal($fields, $condition, $param): bool|static + { + if (empty($param)) { + return true; + } + if ($this->hasPrimary()) { + $condition = [$this->getPrimary() => $this->getPrimaryValue()]; + } + $generate = SqlBuilder::builder(static::find()->where($condition))->update($param); + if (is_bool($generate)) { + return $generate; + } + $command = static::getDb()->createCommand($generate[0], $generate[1]); + if ($command->save(false, $this)) { + return $this->refresh()->afterSave($fields, $param); + } + return false; + } + + /** + * @param null $data + * @return bool|$this + * @throws Exception + */ + public function save($data = NULL): static|bool + { + if (!is_null($data)) { + $this->_attributes = merge($this->_attributes, $data); + } + if (!$this->validator($this->rules()) || !$this->beforeSave($this)) { + return false; + } + [$change, $condition, $fields] = $this->separation(); + if (!$this->isNewExample) { + return $this->updateInternal($fields, $condition, $change); + } + return $this->insert($change, $fields); + } + + + /** + * @param array $rule + * @return bool + * @throws Exception + */ + public function validator(array $rule): bool + { + if (empty($rule)) return true; + $validate = $this->resolve($rule); + if (!$validate->validation()) { + return $this->addError($validate->getError(), 'mysql'); + } else { + return TRUE; + } + } + + /** + * @param $rule + * @return Validator + * @throws Exception + */ + private function resolve($rule): Validator + { + $validate = Validator::getInstance(); + $validate->setParams($this->_attributes); + $validate->setModel($this); + foreach ($rule as $val) { + $field = array_shift($val); + if (empty($val)) { + continue; + } + $validate->make($field, $val); + } + return $validate; + } + + /** + * @param string $name + * @return null + * @throws Exception + */ + public function getAttribute(string $name) + { + if ($this->hasAnnotation($name)) { + return $this->runAnnotation($name, $this->_attributes[$name]); + } + return $this->_attributes[$name] ?? null; + } + + + /** + * @param string $name + * @param mixed $value + * @param string $type + * @return mixed + */ + protected function runAnnotation(string $name, mixed $value, string $type = self::GET): mixed + { + return call_user_func($this->_annotations[$type][$name], $value); + } + + + /** + * @return array + * @throws Exception + */ + private function separation(): array + { + $_tmp = []; + $condition = []; + foreach ($this->_attributes as $key => $val) { + $oldValue = $this->_oldAttributes[$key] ?? null; + if ($val === $oldValue) { + $condition[$key] = $val; + } else { + $_tmp[$key] = $val; + } + } + return [$_tmp, $condition, array_keys($_tmp)]; + } + + + /** + * @param $columns + * @param $format + * @param $key + * @param $value + * @return mixed + */ + public function toFormat($columns, $format, $key, $value): mixed + { + if (isset($format[$key])) { + return $columns->encode($value, $columns->clean($format[$key])); + } + return $value; + } + + + /** + * @param $name + * @param $value + */ + public function setRelate($name, $value) + { + $this->_relate[$name] = $value; + } + + + /** + * @param $name + * @return bool + */ + public function hasRelate($name): bool + { + return isset($this->_relate[$name]); + } + + + /** + * @param array $relates + */ + public function setRelates(array $relates) + { + if (empty($relates)) { + return; + } + foreach ($relates as $key => $val) { + $this->setRelate($key, $val); + } + } + + /** + * @return array + */ + public function getRelates(): array + { + return $this->_relate; + } + + + /** + * @return Relation|null + */ + public function getRelation(): ?Relation + { + return $this->_relation; + } + + + /** + * @param $name + * @return array|string|null + * @throws Exception + */ + public function getRelate($name): null|array|string + { + return Kiri::getAnnotation()->getRelateMethods(static::class, $name); + } + + + /** + * @param $attribute + * @return bool + * @throws Exception + */ + public function has($attribute): bool + { + return static::getColumns()->hasField($attribute); + } + + /**ƒ + * @return string + * @throws Exception + */ + public static function getTable(): string + { + $tablePrefix = static::getDb()->tablePrefix; + + $table = static::tableName(); + if (empty($table)) { + throw new Exception('You need add static method `tableName` and return table name.'); + } + $table = trim($table, '{{%}}'); + if (!empty($tablePrefix) && !str_starts_with($table, $tablePrefix)) { + $table = $tablePrefix . $table; + } + return '`' . static::getDbName() . '`.' . $table; + } + + + /** + * @param $attributes + * @param $changeAttributes + * @return bool + * @throws Exception + */ + #[Event(self::AFTER_SAVE)] + public function afterSave($attributes, $changeAttributes): bool + { + return true; + } + + + /** + * @param $model + * @return bool + */ + #[Event(self::BEFORE_SAVE)] + public function beforeSave($model): bool + { + return true; + } + + + private static string $ab_name = ''; + + + /** + * @return Connection + * @throws Exception + */ + public static function getDb(): Connection + { + return static::setDatabaseConnect('db'); + } + + /** + * @return static + */ + public function refresh(): static + { + $this->_oldAttributes = $this->_attributes; + return $this; + } + + /** + * @param $name + * @param $value + * @throws Exception + */ + public function __set($name, $value) + { + if (method_exists($this, 'set' . ucfirst($name))) { + $this->{'set' . ucfirst($name)}($value); + } else { + $method = $this->_get_annotation($name, self::SET); + if (!empty($method)) { + $value = $this->{$method}($value); + } + $this->_attributes[$name] = $value; + } + } + + + /** + * @param string|null $name + * @param string $method + * @return string|null + * @throws Exception + */ + protected function _get_annotation(string $name = null, string $method = self::GET): ?string + { + $annotation = Kiri::app()->getAnnotation(); + if ($method == static::SET) { + return $annotation->getSetMethodName(static::class, $name); + } + if ($method == static::GET) { + return $annotation->getGetMethodName(static::class, $name); + } + return $annotation->getRelateMethods(static::class, $name); + } + + + /** + * @param $name + * @return mixed + * @throws Exception + */ + public function __get($name): mixed + { + $method = 'get' . ucfirst($name); + if (method_exists($this, $method)) { + return $this->{$method}(); + } + if (isset($this->_attributes[$name])) { + return $this->runGetter($name, $this->_attributes[$name]); + } else { + return $this->runRelation($name); + } + } + + + /** + * @param $name + * @param $value + * @return void|null + * @throws Exception + */ + private function runGetter($name, $value) + { + $relation = $this->_get_annotation($name, static::GET); + if (empty($relation)) { + return $this->_decode($name, $value); + } + return $this->{$relation}($value); + } + + + /** + * @param $name + * @return mixed + * @throws Exception + */ + private function runRelation($name): mixed + { + $relation = $this->_get_annotation($name, static::RELATE); + if (empty($relation)) { + return null; + } + if (($value = $this->{$relation}()) instanceof HasBase) { + return $value->get(); + } + return $value; + } + + + /** + * @param $name + * @param $value + * @return mixed + * @throws Exception + */ + private function _decode($name, $value): mixed + { + return static::getColumns()->_decode($name, $value); + } + + + /** + * @param $name + * @return mixed + */ + private function with($name): mixed + { + $data = $this->{$this->_relate[$name]}(); + if ($data instanceof HasBase) { + return $data->get(); + } + return $data; + } + + + /** + * @param string $type + * @return array + */ + protected function getAnnotation(string $type = self::GET): array + { + return $this->_annotations[$type] ?? []; + } + + + /** + * @param $name + * @param string $type + * @return bool + */ + protected function hasAnnotation($name, string $type = self::GET): bool + { + if (!isset($this->_annotations[$type])) { + return false; + } + return isset($this->_annotations[$type][$name]); + } + + + /** + * @param $item + * @param $data + * @return array + */ + protected function resolveAttributes($item, $data): array + { + return call_user_func($item, $data); + } + + /** + * @param $name + * @return bool + */ + public function __isset($name): bool + { + return isset($this->_attributes[$name]); + } + + /** + * @param $call + * @return mixed + * @throws Exception + */ + private function resolveClass($call): mixed + { + if ($call instanceof HasOne) { + return $call->get(); + } else if ($call instanceof HasMany) { + return $call->get(); + } else { + return $call; + } + } + + + /** + * @param mixed $offset + * @return bool + * @throws Exception + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->_attributes[$offset]) || isset($this->_oldAttributes[$offset]); + } + + /** + * @param mixed $offset + * @return mixed + * @throws Exception + */ + public function offsetGet(mixed $offset): mixed + { + return $this->__get($offset); + } + + /** + * @param mixed $offset + * @param mixed $value + * @throws Exception + */ + public function offsetSet(mixed $offset, mixed $value) + { + $this->__set($offset, $value); + } + + /** + * @param mixed $offset + * @throws Exception + */ + public function offsetUnset(mixed $offset) + { + if (!isset($this->_attributes[$offset]) + && !isset($this->_oldAttributes[$offset])) { + return; + } + unset($this->_attributes[$offset]); + unset($this->_oldAttributes[$offset]); + if (isset($this->_relate)) { + unset($this->_relate[$offset]); + } + } + + /** + * @return array + */ + public function unset(): array + { + $fields = func_get_args(); + $fields = array_shift($fields); + if (!is_array($fields)) { + $fields = explode(',', $fields); + } + + $array = array_combine($fields, $fields); + + return array_diff_assoc($array, $this->_attributes); + } + + + /** + * @param $dbName + * @return mixed + * @throws Exception + */ + public static function setDatabaseConnect($dbName): Connection + { + return Kiri::app()->db->get(static::$connection = $dbName); + } + + + /** + * @return string + * @throws ConfigException + */ + public static function getDbName(): string + { + return Config::get('databases.connections.' . static::$connection . '.database'); + } + + + /** + * @return Columns + * @throws Exception + */ + public static function getColumns(): Columns + { + return static::getDb()->getSchema() + ->getColumns() + ->table(static::getTable()); + } + + /** + * @param array $data + * @return static + * @throws + */ + public static function populate(array $data): static + { + $model = duplicate(static::class); + $model->_attributes = $data; + $model->_oldAttributes = $data; + $model->setIsCreate(false); + return $model; + } + +} diff --git a/src/Base/CollectionIterator.php b/src/Base/CollectionIterator.php new file mode 100644 index 0000000..3025502 --- /dev/null +++ b/src/Base/CollectionIterator.php @@ -0,0 +1,75 @@ +query); + } + + + /** + * CollectionIterator constructor. + * @param $model + * @param $query + * @param array $array + * @param int $flags + * @throws Exception + */ + public function __construct($model, $query, array $array = [], int $flags = 0) + { + $this->model = $model; + $this->query = $query; + parent::__construct($array, $flags); + } + + + /** + * @param $current + * @return ActiveRecord + * @throws Exception + */ + protected function newModel($current): ActiveRecord + { + return $this->model->setAttributes($current); + } + + + /** + * @throws Exception + */ + public function current(): ActiveRecord + { + if (is_array($current = parent::current())) { + $current = $this->newModel($current); + } + return $this->query->getWith($current); + } + + +} diff --git a/src/Base/ConditionClassMap.php b/src/Base/ConditionClassMap.php new file mode 100644 index 0000000..cb3eaa0 --- /dev/null +++ b/src/Base/ConditionClassMap.php @@ -0,0 +1,71 @@ + [ + 'class' => InCondition::class + ], + 'NOT IN' => [ + 'class' => NotInCondition::class + ], + 'LIKE' => [ + 'class' => LikeCondition::class + ], + 'NOT LIKE' => [ + 'class' => NotLikeCondition::class + ], + 'LLike' => [ + 'class' => LLikeCondition::class + ], + 'RLike' => [ + 'class' => RLikeCondition::class + ], + 'EQ' => [ + 'class' => MathematicsCondition::class, + 'type' => 'EQ' + ], + 'NEQ' => [ + 'class' => MathematicsCondition::class, + 'type' => 'NEQ' + ], + 'GT' => [ + 'class' => MathematicsCondition::class, + 'type' => 'GT' + ], + 'EGT' => [ + 'class' => MathematicsCondition::class, + 'type' => 'EGT' + ], + 'LT' => [ + 'class' => MathematicsCondition::class, + 'type' => 'LT' + ], + 'ELT' => [ + 'class' => MathematicsCondition::class, + 'type' => 'ELT' + ], + 'BETWEEN' => BetweenCondition::class, + 'NOT BETWEEN' => NotBetweenCondition::class, + ]; + +} diff --git a/src/Collection.php b/src/Collection.php new file mode 100644 index 0000000..acfe4de --- /dev/null +++ b/src/Collection.php @@ -0,0 +1,242 @@ +_item; + } + + /** + * @param $field + * + * @return array|null + * @throws Exception + */ + public function values($field): ?array + { + if (empty($field) || !is_string($field)) { + return NULL; + } + $_tmp = []; + $data = $this->toArray(); + foreach ($data as $val) { + /** @var ActiveRecord $val */ + $_tmp[] = $val[$field]; + } + return $_tmp; + } + + /** + * @param string $field + * @return array|null + */ + public function keyBy(string $field): ?array + { + $array = $this->toArray(); + $column = array_flip(array_column($array, $field)); + foreach ($column as $key => $value) { + $column[$key] = $array[$value]; + } + + return $column; + } + + /** + * @return $this + */ + public function orderRand(): static + { + shuffle($this->_item); + return $this; + } + + /** + * @param int $start + * @param int $length + * + * @return array + */ + #[Pure] public function slice(int $start = 0, int $length = 20): array + { + if (empty($this->_item) || !is_array($this->_item)) { + return []; + } + if (count($this->_item) < $length) { + return $this->_item; + } else { + return array_slice($this->_item, $start, $length); + } + } + + /** + * @param $field + * @param string $setKey + * + * @return array|null + */ + public function column(string $field, string $setKey = ''): ?array + { + $data = $this->toArray(); + if (empty($data)) { + return []; + } + if (!empty($setKey) && is_string($setKey)) { + return array_column($data, $field, $setKey); + } else { + return array_column($data, $field); + } + } + + /** + * @param string $field + * + * @return float|int|null + */ + public function sum(string $field): float|int|null + { + $array = $this->column($field); + if (empty($array)) { + return NULL; + } + return array_sum($array); + } + + /** + * @return ActiveRecord|array + */ + #[Pure] public function current(): ActiveRecord|array + { + return current($this->_item); + } + + /** + * @return int + */ + #[Pure] public function size(): int + { + return (int)count($this->_item); + } + + /** + * @return array + * @throws + */ + public function toArray(): array + { + $array = []; + foreach ($this as $value) { + if (!is_object($value)) { + continue; + } + $array[] = $value->toArray(); + } + $this->_item = []; + return $array; + } + + /** + * @throws Exception + * 批量删除 + */ + public function delete(): bool + { + $model = $this->getModel(); + if (!$model->hasPrimary()) return false; + $ids = []; + foreach ($this as $item) { + $id = $item->getPrimaryValue(); + if (!empty($id)) { + $ids[] = $id; + } + } + return $model::find()->whereIn($model->getPrimary(), $ids)->delete(); + } + + /** + * @param array $condition + * @return Collection + * @throws + */ + public function filter(array $condition): Collection|static + { + $_filters = []; + if (empty($condition)) { + return $this; + } + foreach ($this as $value) { + if (!$this->filterCheck($value, $condition)) { + continue; + } + $_filters[] = $value; + } + return new Collection($this->query, $_filters, $this->model); + } + + + /** + * @param $value + * @param $condition + * @return bool + * @throws Exception + */ + private function filterCheck($value, $condition): bool + { + $_value = $value; + if ($_value instanceof ActiveRecord) { + $_value = $_value->toArray(); + } + $_tmp = array_intersect_key($_value, $condition); + if (count(array_diff_assoc($_tmp, $condition)) > 0) { + return false; + } + return true; + } + + + /** + * @param $key + * @param $value + * @return mixed + */ + public function exists($key, $value): mixed + { + foreach ($this as $item) { + if ($item->$key === $value) { + return $item; + } + } + return null; + } + + /** + * @return bool + */ + #[Pure] public function isEmpty(): bool + { + return $this->size() < 1; + } +} diff --git a/src/Command.php b/src/Command.php new file mode 100644 index 0000000..83a1fb4 --- /dev/null +++ b/src/Command.php @@ -0,0 +1,318 @@ +execute(static::EXECUTE); + } + + /** + * @param bool $isInsert + * @param mixed $hasAutoIncrement + * @return int|bool|array|string|null + * @throws Exception + */ + public function save(bool $isInsert = TRUE, mixed $hasAutoIncrement = null): int|bool|array|string|null + { + return $this->execute(static::EXECUTE, $isInsert, $hasAutoIncrement); + } + + + /** + * @return int|bool|array|string|null + * @throws Exception + */ + public function all(): int|bool|array|string|null + { + return $this->execute(static::FETCH_ALL); + } + + /** + * @return array|bool|int|string|null + * @throws Exception + */ + public function one(): null|array|bool|int|string + { + return $this->execute(static::FETCH); + } + + /** + * @return int|bool|array|string|null + * @throws Exception + */ + public function fetchColumn(): int|bool|array|string|null + { + return $this->execute(static::FETCH_COLUMN); + } + + /** + * @return int|bool|array|string|null + * @throws Exception + */ + public function rowCount(): int|bool|array|string|null + { + return $this->execute(static::ROW_COUNT); + } + + /** + * @return int|bool|array|string|null + * @throws Exception + */ + public function flush(): int|bool|array|string|null + { + return $this->execute(static::EXECUTE); + } + + /** + * @param $type + * @param null $isInsert + * @param bool|null $hasAutoIncrement + * @return int|bool|array|string|null + * @throws Exception + */ + private function execute($type, $isInsert = null, mixed $hasAutoIncrement = null): int|bool|array|string|null + { + try { + $time = microtime(true); + if ($type === static::EXECUTE) { + $result = $this->insert_or_change($isInsert, $hasAutoIncrement); + } else { + $result = $this->search($type); + } + if (microtime(true) - $time >= 0.02) { + $this->warning('Mysql:' . Json::encode([$this->sql, $this->params]) . (microtime(true) - $time)); + } + $this->prepare?->closeCursor(); + } catch (\Throwable $exception) { + $result = $this->addError($this->sql . '. error: ' . $exception->getMessage(), 'mysql'); + } finally { + $this->db->release(); + return $result; + } + } + + + /** + * @param $type + * @return mixed + * @throws Exception + */ + private function search($type): mixed + { + if (($prepare = $this->prepare()) == false) { + return false; + } + if ($type === static::FETCH_COLUMN) { + $data = $prepare->fetchAll(PDO::FETCH_ASSOC); + } else if ($type === static::ROW_COUNT) { + $data = $prepare->rowCount(); + } else if ($type === static::FETCH_ALL) { + $data = $prepare->fetchAll(PDO::FETCH_ASSOC); + } else { + $data = $prepare->fetch(PDO::FETCH_ASSOC); + } + $prepare->closeCursor(); + return $data; + } + + + /** + * @param $isInsert + * @param $hasAutoIncrement + * @return bool|string|int + * @throws Exception + */ + private function insert_or_change($isInsert, $hasAutoIncrement): bool|string|int + { + if (($result = $this->getPdoStatement()) === false) { + return $result; + } + if ($isInsert === false || !$hasAutoIncrement) { + return true; + } + if ($result == 0 && $hasAutoIncrement->isAutoIncrement()) { + return $this->addError(static::DB_ERROR_MESSAGE, 'mysql'); + } + return $result == 0 ? true : $result; + } + + + /** + * 重新构建 + * @throws + */ + private function getPdoStatement(): bool|int + { + if (empty($this->sql)) { + return $this->addError('no sql.', 'mysql'); + } + if (!(($connect = $this->db->getConnect($this->sql)) instanceof PDO)) { + return $this->addError('get client error.', 'mysql'); + } + if (!(($prepare = $connect->prepare($this->sql)) instanceof PDOStatement)) { + return $this->addError($this->errorMessage($prepare), 'mysql'); + } + $result = $this->checkResponse($prepare, $connect); + $prepare->closeCursor(); + return $result; + } + + + /** + * @param $prepare + * @return string + */ + private function errorMessage($prepare): string + { + return $this->sql . ':' . ($prepare->errorInfo()[2] ?? static::DB_ERROR_MESSAGE); + } + + + /** + * @return bool|\PDOStatement + * @throws \Exception + */ + private function prepare(): bool|PDOStatement + { + if (!(($connect = $this->db->getConnect($this->sql)) instanceof PDO)) { + return $this->addError('get client error.', 'mysql'); + } + if (!(($prepare = $connect->query($this->sql)) instanceof PDOStatement)) { + $error = $prepare->errorInfo()[2] ?? static::DB_ERROR_MESSAGE; + return $this->addError($this->sql . ':' . $error, 'mysql'); + } + return $prepare; + } + + + /** + * @param $prepare + * @param $connect + * @return bool|int + * @throws \Exception + */ + private function checkResponse($prepare, $connect): bool|int + { + $result = $prepare->execute($this->params); + if ($result === false) { + return $this->addError($connect->errorInfo()[2], 'mysql'); + } + return (int)$connect->lastInsertId(); + } + + + /** + * @param $modelName + * @return $this + */ + public function setModelName($modelName): static + { + $this->_modelName = $modelName; + return $this; + } + + /** + * @return string + */ + public function getModelName(): string + { + return $this->_modelName; + } + + /** + * @return int|bool|array|string|null + * @throws Exception + */ + public function delete(): int|bool|array|string|null + { + return $this->execute(static::EXECUTE); + } + + /** + * @param null $scope + * @param bool $insert + * @return int|bool|array|string|null + * @throws Exception + */ + public function exec($scope = null, bool $insert = false): int|bool|array|string|null + { + return $this->execute(static::EXECUTE, $insert, $scope); + } + + /** + * @param array $data + * @return $this + */ + public function bindValues(array $data = []): static + { + if (!is_array($this->params)) { + $this->params = []; + } + if (!empty($data)) { + $this->params = array_merge($this->params, $data); + } + return $this; + } + + /** + * @param $sql + * @return $this + * @throws Exception + */ + public function setSql($sql): static + { + $this->sql = $sql; + return $this; + } + +} diff --git a/src/Condition/BetweenCondition.php b/src/Condition/BetweenCondition.php new file mode 100644 index 0000000..017715c --- /dev/null +++ b/src/Condition/BetweenCondition.php @@ -0,0 +1,23 @@ +column . ' BETWEEN ' . (int)$this->value[0] . ' AND ' . (int)$this->value[1]; + } + +} diff --git a/src/Condition/ChildCondition.php b/src/Condition/ChildCondition.php new file mode 100644 index 0000000..7280750 --- /dev/null +++ b/src/Condition/ChildCondition.php @@ -0,0 +1,22 @@ +column . ' ' . $this->opera . ' (' . $this->value . ')'; + } + +} diff --git a/src/Condition/Condition.php b/src/Condition/Condition.php new file mode 100644 index 0000000..a331a4e --- /dev/null +++ b/src/Condition/Condition.php @@ -0,0 +1,83 @@ +column = $column; + } + + /** + * @param string $opera + */ + public function setOpera(string $opera): void + { + $this->opera = $opera; + } + + /** + * @param $params + */ + public function setValue($params): void + { + if (is_array($params)) { + $values = []; + foreach ($params as $item => $value) { + $values[$item] = is_numeric($value) ? $value : '\'' . $value . '\''; + } + $this->value = $values; + } else { + $this->value = $this->checkIsSqlString($params); + } + } + + + /** + * @param $params + * @return int|string + */ + #[Pure] private function checkIsSqlString($params): int|string + { + if (is_numeric($params)) { + return $params; + } + + $check = ltrim($params, '('); + $check = strtolower(substr($check, 0, 6)); + if (in_array($check, ['update', 'select', 'insert', 'delete'])) { + return $params; + } else { + return sprintf('\'%s\'', $params); + } + } + + +} diff --git a/src/Condition/DefaultCondition.php b/src/Condition/DefaultCondition.php new file mode 100644 index 0000000..79971e7 --- /dev/null +++ b/src/Condition/DefaultCondition.php @@ -0,0 +1,24 @@ +column, $this->opera, addslashes($this->value)); + } + +} diff --git a/src/Condition/HashCondition.php b/src/Condition/HashCondition.php new file mode 100644 index 0000000..887c3f6 --- /dev/null +++ b/src/Condition/HashCondition.php @@ -0,0 +1,31 @@ +value)) { + return ''; + } + foreach ($this->value as $key => $value) { + if ($value === null) { + continue; + } + $array[] = sprintf("%s = '%s'", $key, addslashes($value)); + } + return implode(' AND ', $array); + } + +} diff --git a/src/Condition/InCondition.php b/src/Condition/InCondition.php new file mode 100644 index 0000000..c11cb48 --- /dev/null +++ b/src/Condition/InCondition.php @@ -0,0 +1,31 @@ +value)) { + return sprintf('%s IN (%s)', $this->column, implode(',', $this->value)); + } else { + return sprintf('%s IN (%s)', $this->column, $this->value); + } + } + +} diff --git a/src/Condition/JsonCondition.php b/src/Condition/JsonCondition.php new file mode 100644 index 0000000..48c8166 --- /dev/null +++ b/src/Condition/JsonCondition.php @@ -0,0 +1,20 @@ +value)) { + $this->value = array_shift($this->value); + } + return $this->column . ' LIKE \'%' . addslashes($this->value) . '\''; + } + +} diff --git a/src/Condition/LikeCondition.php b/src/Condition/LikeCondition.php new file mode 100644 index 0000000..acb99f0 --- /dev/null +++ b/src/Condition/LikeCondition.php @@ -0,0 +1,28 @@ +value)) { + $this->value = array_shift($this->value); + } + return $this->column . ' LIKE \'%' . addslashes($this->value) . '%\''; + } + +} diff --git a/src/Condition/MathematicsCondition.php b/src/Condition/MathematicsCondition.php new file mode 100644 index 0000000..b5c45ca --- /dev/null +++ b/src/Condition/MathematicsCondition.php @@ -0,0 +1,78 @@ +{strtolower($this->type)}((float)$this->value); + } + + /** + * @param $value + * @return string + */ + public function eq($value): string + { + return $this->column . ' = ' . $value; + } + + /** + * @param $value + * @return string + */ + public function neq($value): string + { + return $this->column . ' <> ' . $value; + } + + /** + * @param $value + * @return string + */ + public function gt($value): string + { + return $this->column . ' > ' . $value; + } + + /** + * @param $value + * @return string + */ + public function egt($value): string + { + return $this->column . ' >= ' . $value; + } + + + /** + * @param $value + * @return string + */ + public function lt($value): string + { + return $this->column . ' < ' . $value; + } + + /** + * @param $value + * @return string + */ + public function elt($value): string + { + return $this->column . ' <= ' . $value; + } + +} diff --git a/src/Condition/NotBetweenCondition.php b/src/Condition/NotBetweenCondition.php new file mode 100644 index 0000000..872083b --- /dev/null +++ b/src/Condition/NotBetweenCondition.php @@ -0,0 +1,22 @@ +column . ' NOT BETWEEN ' . (int)$this->value[0] . ' AND ' . (int)$this->value[1]; + } + +} diff --git a/src/Condition/NotInCondition.php b/src/Condition/NotInCondition.php new file mode 100644 index 0000000..f5e0165 --- /dev/null +++ b/src/Condition/NotInCondition.php @@ -0,0 +1,28 @@ +value)) { + return null; + } + $value = '\'' . implode('\',\'', $this->value) . '\''; + return '`' . $this->column . '` not in(' . $value . ')'; + } + +} diff --git a/src/Condition/NotLikeCondition.php b/src/Condition/NotLikeCondition.php new file mode 100644 index 0000000..9787d62 --- /dev/null +++ b/src/Condition/NotLikeCondition.php @@ -0,0 +1,28 @@ +value)) { + $this->value = array_shift($this->value); + } + return $this->column . ' NOT LIKE \'%' . addslashes($this->value) . '%\''; + } + +} diff --git a/src/Condition/OrCondition.php b/src/Condition/OrCondition.php new file mode 100644 index 0000000..06fee97 --- /dev/null +++ b/src/Condition/OrCondition.php @@ -0,0 +1,27 @@ +oldParams), addslashes($this->value)); + } + +} diff --git a/src/Condition/RLikeCondition.php b/src/Condition/RLikeCondition.php new file mode 100644 index 0000000..4f3aec6 --- /dev/null +++ b/src/Condition/RLikeCondition.php @@ -0,0 +1,28 @@ +value)) { + $this->value = array_shift($this->value); + } + return sprintf('%s LIKE \'%s\'', $this->column, addslashes($this->value)); + } + +} diff --git a/src/Connection.php b/src/Connection.php new file mode 100644 index 0000000..0f695ad --- /dev/null +++ b/src/Connection.php @@ -0,0 +1,341 @@ +eventProvider->on(OnWorkerStop::class, [$this, 'clear_connection'], 0); + $this->eventProvider->on(OnWorkerExit::class, [$this, 'clear_connection'], 0); + $this->eventProvider->on(BeginTransaction::class, [$this, 'beginTransaction'], 0); + $this->eventProvider->on(Rollback::class, [$this, 'rollback'], 0); + $this->eventProvider->on(Commit::class, [$this, 'commit'], 0); + + if (Db::transactionsActive()) { + $this->beginTransaction(); + } + + $this->_schema->db = $this; + } + + + /** + * @param null $sql + * @return PDO + * @throws Exception + */ + public function getConnect($sql = NULL): PDO + { + return $this->getPdo($sql); + } + + + /** + * @throws Exception + */ + public function fill() + { + $connections = $this->connections(); + $pool = Config::get('databases.pool.max', 10); + + $connections->initConnections('Mysql:' . $this->cds, true, $pool); + if (!empty($this->slaveConfig) && $this->cds != $this->slaveConfig['cds']) { + $connections->initConnections('Mysql:' . $this->slaveConfig['cds'], false, $pool); + } + } + + + /** + * @param $sql + * @return PDO + * @throws Exception + */ + private function getPdo($sql): PDO + { + if ($this->isWrite($sql)) { + return $this->masterInstance(); + } else { + return $this->slaveInstance(); + } + } + + /** + * @return mixed + * @throws ReflectionException + * @throws NotFindClassException + */ + public function getSchema(): Schema + { + if ($this->_schema === null) { + $this->_schema = Kiri::createObject([ + 'class' => Schema::class, + 'db' => $this + ]); + } + return $this->_schema; + } + + /** + * @param $sql + * @return bool + */ + #[Pure] public function isWrite($sql): bool + { + if (empty($sql)) return false; + if (str_starts_with(strtolower($sql), 'select')) { + return false; + } + return true; + } + + /** + * @return mixed + * @throws Exception + */ + public function getCacheDriver(): mixed + { + if (!$this->enableCache) { + return null; + } + return Kiri::app()->get($this->cacheDriver); + } + + /** + * @return PDO + * @throws Exception + */ + public function masterInstance(): PDO + { + return $this->connections()->get([ + 'cds' => $this->cds, + 'username' => $this->username, + 'password' => $this->password, + 'database' => $this->database + ], true); + } + + /** + * @return PDO + * @throws Exception + */ + public function slaveInstance(): PDO + { + if (empty($this->slaveConfig) || Db::transactionsActive()) { + return $this->masterInstance(); + } + if ($this->slaveConfig['cds'] == $this->cds) { + return $this->masterInstance(); + } + return $this->connections()->get($this->slaveConfig, false); + } + + + /** + * @return \Kiri\Pool\Connection + * @throws Exception + */ + private function connections(): \Kiri\Pool\Connection + { + return Kiri::getDi()->get(\Kiri\Pool\Connection::class); + } + + + /** + * @return $this + * @throws Exception + */ + public function beginTransaction(): static + { + $this->connections()->beginTransaction($this->cds); + return $this; + } + + /** + * @return $this|bool + * @throws Exception + */ + public function inTransaction(): bool|static + { + return $this->connections()->inTransaction($this->cds); + } + + /** + * @throws Exception + * 事务回滚 + */ + public function rollback() + { + $this->connections()->rollback($this->cds); + $this->release(); + } + + /** + * @throws Exception + * 事务提交 + */ + public function commit() + { + $this->connections()->commit($this->cds); + $this->release(); + } + + /** + * @param $sql + * @return PDO + * @throws Exception + */ + public function refresh($sql): PDO + { + if ($this->isWrite($sql)) { + $instance = $this->masterInstance(); + } else { + $instance = $this->slaveInstance(); + } + return $instance; + } + + /** + * @param null $sql + * @param array $attributes + * @return Command + * @throws Exception + */ + public function createCommand($sql = null, array $attributes = []): Command + { + $command = new Command(['db' => $this, 'sql' => $sql]); + return $command->bindValues($attributes); + } + + + /** + * + * 回收链接 + * @throws + */ + public function release() + { + if (!Kiri::isWorker() && !Kiri::isProcess()) { + $this->clear_connection(); + return; + } + $connections = $this->connections(); + $connections->release($this->cds, true); + $connections->release($this->slaveConfig['cds'], false); + } + + + /** + * @throws Exception + */ + public function recovery() + { + $connections = $this->connections(); + + $connections->release($this->cds, true); + $connections->release($this->slaveConfig['cds'], false); + } + + /** + * + * 回收链接 + * @throws + */ + public function clear_connection() + { + $connections = $this->connections(); + + $connections->disconnect($this->cds, true); + $connections->disconnect($this->slaveConfig['cds'], false); + } + + + /** + * @throws Exception + */ + public function disconnect() + { + $connections = $this->connections(); + $connections->disconnect($this->cds, true); + $connections->disconnect($this->slaveConfig['cds'], false); + } + +} diff --git a/src/DatabasesProviders.php b/src/DatabasesProviders.php new file mode 100644 index 0000000..12bcf8a --- /dev/null +++ b/src/DatabasesProviders.php @@ -0,0 +1,116 @@ + 0, 'max' => 1]; + + + /** + * @var EventProvider + */ + #[Inject(EventProvider::class)] + public EventProvider $eventProvider; + + + /** + * @param Application $application + * @throws Exception + */ + public function onImport(Application $application) + { + $application->set('db', $this); + + $this->_pooLength = Config::get('databases.pool', ['min' => 0, 'max' => 1]); + + $this->eventProvider->on(OnWorkerStart::class, [$this, 'createPool']); + } + + + /** + * @param $name + * @return Connection + * @throws ConfigException + * @throws Exception + */ + public function get($name): Connection + { + $application = Kiri::app(); + if (!$application->has('databases.' . $name)) { + $application->set('databases.' . $name, $this->_settings($this->getConfig($name))); + } + return $application->get('databases.' . $name); + } + + + /** + * @throws ConfigException + * @throws Exception + */ + public function createPool(OnWorkerStart $onWorkerStart) + { + $databases = Config::get('databases.connections', []); + if (empty($databases)) { + return; + } + $application = Kiri::app(); + foreach ($databases as $name => $database) { + /** @var Connection $connection */ + $application->set('databases.' . $name, $this->_settings($database)); + $application->get('databases.' . $name)->fill(); + } + } + + + /** + * @param $database + * @return array + */ + private function _settings($database): array + { + return [ + 'class' => Connection::class, + 'id' => $database['id'], + 'cds' => $database['cds'], + 'username' => $database['username'], + 'password' => $database['password'], + 'tablePrefix' => $database['tablePrefix'], + 'database' => $database['database'], + 'maxNumber' => $this->_pooLength['max'], + 'minNumber' => $this->_pooLength['min'], + 'charset' => $database['charset'] ?? 'utf8mb4', + 'slaveConfig' => $database['slaveConfig'] + ]; + } + + + /** + * @param $name + * @return mixed + * @throws ConfigException + */ + public function getConfig($name): mixed + { + return Config::get('databases.connections.' . $name, null, true); + } + + +} diff --git a/src/Db.php b/src/Db.php new file mode 100644 index 0000000..dbd6232 --- /dev/null +++ b/src/Db.php @@ -0,0 +1,365 @@ +dispatch(new BeginTransaction()); + } + static::$_inTransaction = true; + } + + + /** + * @throws Exception + */ + public static function commit() + { + if (static::transactionsActive()) { + di(EventDispatch::class)->dispatch(new Commit()); + } + static::$_inTransaction = false; + } + + + /** + * @throws Exception + */ + public static function rollback() + { + if (static::transactionsActive()) { + di(EventDispatch::class)->dispatch(new Rollback()); + } + static::$_inTransaction = false; + } + + + /** + * @param $table + * + * @return static + */ + public static function table($table): Db|static + { + $connection = new Db(); + $connection->from($table); + return $connection; + } + + + /** + * @param string $column + * @param string $alias + * @return string + */ + public static function any_value(string $column, string $alias = ''): string + { + if (empty($alias)) { + $alias = $column . '_any_value'; + } + return 'ANY_VALUE(' . $column . ') as ' . $alias; + } + + + /** + * @param string $column + * @return string + */ + public static function increment(string $column): string + { + return '+ ' . $column; + } + + + /** + * @param string $column + * @return string + */ + public static function decrement(string $column): string + { + return '- ' . $column; + } + + + /** + * @param Connection|null $connection + * @return mixed + * @throws Exception + */ + public function get(Connection $connection = NULL): mixed + { + $connection = static::getDefaultConnection($connection); + + return $connection->createCommand(SqlBuilder::builder($this)->one()) + ->all(); + } + + /** + * @param $column + * @return string + */ + public static function raw($column): string + { + return '`' . $column . '`'; + } + + /** + * @param Connection|null $connection + * @return mixed + * @throws Exception + */ + public function find(Connection $connection = NULL): mixed + { + $connection = static::getDefaultConnection($connection); + + return $connection->createCommand(SqlBuilder::builder($this)->all()) + ->one(); + } + + /** + * @param Connection|NULL $connection + * @return bool|int + * @throws Exception + */ + public function count(Connection $connection = NULL): bool|int + { + $connection = static::getDefaultConnection($connection); + + return $connection->createCommand(SqlBuilder::builder($this)->count()) + ->exec(); + } + + /** + * @param Connection|NULL $connection + * @return bool|int + * @throws Exception + */ + public function exists(Connection $connection = NULL): bool|int + { + $connection = static::getDefaultConnection($connection); + + return $connection->createCommand(SqlBuilder::builder($this)->one()) + ->fetchColumn(); + } + + /** + * @param string $sql + * @param array $attributes + * @param Connection|null $connection + * @return array|bool|int|string|null + * @throws Exception + */ + public static function findAllBySql(string $sql, array $attributes = [], Connection $connection = NULL): int|bool|array|string|null + { + $connection = static::getDefaultConnection($connection); + + return $connection->createCommand($sql, $attributes)->all(); + } + + /** + * @param string $sql + * @param array $attributes + * @param Connection|NULL $connection + * @return string|array|bool|int|null + * @throws Exception + */ + public static function findBySql(string $sql, array $attributes = [], Connection $connection = NULL): string|array|bool|int|null + { + $connection = static::getDefaultConnection($connection); + + return $connection->createCommand($sql, $attributes)->one(); + } + + /** + * @param string $field + * @return array|null + * @throws Exception + */ + public function values(string $field): ?array + { + $data = $this->get(); + if (empty($data) || empty($field)) { + return NULL; + } + $first = current($data); + if (!isset($first[$field])) { + return NULL; + } + return array_column($data, $field); + } + + /** + * @param $field + * @return mixed + * @throws Exception + */ + public function value($field): mixed + { + $data = $this->find(); + if (!empty($field) && isset($data[$field])) { + return $data[$field]; + } + return $data; + } + + /** + * @param Connection|null $connection + * @return bool|int + * @throws ConfigException + * @throws Exception + */ + public function delete(?Connection $connection = null): bool|int + { + $connection = static::getDefaultConnection($connection); + + return $connection->createCommand($connection->getBuild()->builder($this))->delete(); + } + + /** + * @param string $table + * @param null $connection + * @return bool|int + * @throws ConfigException + * @throws Exception + */ + public static function drop(string $table, $connection = null): bool|int + { + $connection = static::getDefaultConnection($connection); + + $sprint = sprintf('DROP TABLE `%s`.`%s`', $connection->database, $table); + return $connection->createCommand($sprint)->delete(); + } + + /** + * @param string $table + * @param null $connection + * @return bool|int + * @throws Exception + */ + public static function truncate(string $table, $connection = null): bool|int + { + $connection = static::getDefaultConnection($connection); + + $sprint = sprintf('TRUNCATE `%s`.`%s`', $connection->database, $table); + return $connection->createCommand($sprint)->exec(); + } + + /** + * @param string $table + * @param Connection|NULL $connection + * @return mixed + * @throws ConfigException + * @throws Exception + */ + public static function showCreateSql(string $table, Connection $connection = NULL): mixed + { + $connection = static::getDefaultConnection($connection); + + $sprint = sprintf('SHOW CREATE TABLE `%s`.`%s`', $connection->database, $table); + return $connection->createCommand($sprint)->one(); + } + + /** + * @param string $table + * @param Connection|NULL $connection + * @return bool|int|null + * @throws ConfigException + * @throws Exception + */ + public static function desc(string $table, Connection $connection = NULL): bool|int|null + { + $connection = static::getDefaultConnection($connection); + + $sprint = sprintf('SHOW FULL FIELDS FROM `%s`.`%s`', $connection->database, $table); + return $connection->createCommand($sprint)->all(); + } + + + /** + * @param string $table + * @param Connection|NULL $connection + * @return mixed + * @throws Exception + */ + public static function show(string $table, Connection $connection = NULL): mixed + { + if (empty($table)) { + return null; + } + $connection = static::getDefaultConnection($connection); + + $table = [' const TABLE = \'select * from %s where REFERENCED_TABLE_NAME=%s\';']; + return $connection->createCommand((new Query()) + ->select('*') + ->from('INFORMATION_SCHEMA.KEY_COLUMN_USAGE') + ->where(['REFERENCED_TABLE_NAME' => $table]) + ->getSql())->one(); + } + + + /** + * @param null|Connection $connection + * @param null $name + * @return mixed + * @throws ConfigException + * @throws Exception + */ + public static function getDefaultConnection(?Connection $connection, $name = null): Connection + { + if ($connection instanceof Connection) { + return $connection; + } + $databases = Config::get('databases.connections', []); + if (empty($databases) || !is_array($databases)) { + throw new Exception('Please configure the database link.'); + } + if (!empty($name)) { + if (!isset($databases[$name])) { + throw new Exception('Please configure the database link.'); + } + return $databases[$name]; + } + return current($databases); + } + + +} diff --git a/src/HasCount.php b/src/HasCount.php new file mode 100644 index 0000000..a993fa6 --- /dev/null +++ b/src/HasCount.php @@ -0,0 +1,39 @@ +_relation->getQuery($this->model::className())->$name(...$arguments); + } + + /** + * @return array|null|ActiveRecord + * @throws Exception + */ + public function get(): array|ActiveRecord|null + { + return $this->_relation->count($this->model::className(), $this->value); + } + +} diff --git a/src/HasMany.php b/src/HasMany.php new file mode 100644 index 0000000..24f08d8 --- /dev/null +++ b/src/HasMany.php @@ -0,0 +1,44 @@ +_relation->getQuery($this->model::className())->$name(...$arguments); + } + + /** + * @return array|null|ActiveRecord + * @throws Exception + */ + public function get(): array|ActiveRecord|null + { + return $this->_relation->get($this->model::className(), $this->value); + } +} diff --git a/src/HasOne.php b/src/HasOne.php new file mode 100644 index 0000000..347eec6 --- /dev/null +++ b/src/HasOne.php @@ -0,0 +1,45 @@ +_relation->getQuery($this->model::className())->$name(...$arguments); + return $this; + } + + /** + * @return array|null|ActiveRecord + * @throws Exception + */ + public function get(): array|ActiveRecord|null + { + return $this->_relation->first($this->model::className(), $this->value); + } +} diff --git a/src/IOrm.php b/src/IOrm.php new file mode 100644 index 0000000..4e3d9a5 --- /dev/null +++ b/src/IOrm.php @@ -0,0 +1,50 @@ +structure($this->table = $table); + return $this; + } + + /** + * @return string + */ + public function getTable(): string + { + return $this->table; + } + + /** + * @param $key + * @param $val + * @return mixed + * @throws Exception + */ + public function fieldFormat($key, $val): mixed + { + return $this->encode($val, $this->get_fields($key)); + } + + /** + * @param $data + * @return array + * @throws + */ + public function populate($data): array + { + $column = $this->get_fields(); + foreach ($data as $key => $val) { + if (!isset($column[$key])) { + continue; + } + $data[$key] = $this->decode($val, $column[$key]); + } + return $data; + } + + /** + * @param $val + * @param null $format + * @return mixed + */ + public function decode($val, $format = null): mixed + { + if (empty($format) || $val === null) { + return $val; + } + $format = strtolower($format); + if ($this->isInt($format)) { + return (int)$val; + } else if ($this->isJson($format)) { + return Json::decode($val, true); + } else if ($this->isFloat($format)) { + return (float)$val; + } else { + return stripslashes($val); + } + } + + + /** + * @param string $name + * @param $value + * @return mixed + * @throws Exception + */ + public function _decode(string $name, $value): mixed + { + return $this->decode($value, $this->get_fields($name)); + } + + + /** + * @param $val + * @param null $format + * @return float|bool|int|string + * @throws Exception + */ + public function encode($val, $format = null): float|bool|int|string + { + if (empty($format)) { + return $val; + } + $format = strtolower($format); + if ($this->isInt($format)) { + return (int)$val; + } else if ($this->isJson($format)) { + return Json::encode($val); + } else if ($this->isFloat($format)) { + return (float)$val; + } else { + return addslashes($val); + } + } + + /** + * @param $format + * @return bool + */ + #[Pure] public function isInt($format): bool + { + return in_array($format, ['int', 'bigint', 'tinyint', 'smallint', 'mediumint']); + } + + /** + * @param $format + * @return bool + */ + #[Pure] public function isFloat($format): bool + { + return in_array($format, ['float', 'double', 'decimal']); + } + + /** + * @param $format + * @return bool + */ + #[Pure] public function isJson($format): bool + { + return in_array($format, ['json']); + } + + /** + * @param $format + * @return bool + */ + #[Pure] public function isString($format): bool + { + return in_array($format, ['varchar', 'char', 'text', 'longtext', 'tinytext', 'mediumtext']); + } + + + /** + * @return array + * @throws + */ + public function format(): array + { + return $this->columns('Default', 'Field'); + } + + + /** + * @return mixed + * @throws Exception + */ + public function getFields(): array + { + if (empty($this->_fields)) { + $this->structure($this->table); + } + return $this->_fields[$this->table]; + } + + + /** + * @param string $name + * @return bool + * @throws Exception + */ + public function hasField(string $name): bool + { + return array_key_exists($name, $this->getFields()); + } + + + /** + * @return int|string|null + * @throws Exception + */ + public function getAutoIncrement(): int|string|null + { + return $this->_auto_increment[$this->table] ?? null; + } + + /** + * @return array|null|string + * + * @throws Exception + */ + public function getPrimaryKeys(): array|string|null + { + if (isset($this->_auto_increment[$this->table])) { + return $this->_auto_increment[$this->table]; + } + return $this->_primary[$this->table] ?? null; + } + + /** + * @return array|null|string + * + * @throws Exception + */ + #[Pure] public function getFirstPrimary(): array|string|null + { + if (isset($this->_auto_increment[$this->table])) { + return $this->_auto_increment[$this->table]; + } + if (isset($this->_primary[$this->table])) { + return current($this->_primary[$this->table]); + } + return null; + } + + /** + * @param $name + * @param null $index + * @return array + * @throws Exception + */ + private function columns($name, $index = null): array + { + if (empty($index)) { + return array_column($this->getColumns(), $name); + } else { + return array_column($this->getColumns(), $name, $index); + } + } + + /** + * @return array|static + * @throws Exception + */ + private function getColumns(): array|static + { + return $this->structure($this->getTable()); + } + + + /** + * @param $table + * @return array|Columns + * @throws Exception + */ + private function structure($table): array|static + { + if (!isset($this->columns[$table]) || empty($this->columns[$table])) { + $column = $this->db->createCommand(SqlBuilder::builder(null)->columns($table))->all(); + if (empty($column)) { + throw new Exception("The table " . $table . " not exists."); + } + return $this->columns[$table] = $this->resolve($column, $table); + } + return $this->columns[$table]; + } + + + /** + * @param $column + * @param $table + * @return array + */ + private function resolve(array $column, $table): array + { + foreach ($column as $key => $item) { + $this->addPrimary($item, $table); + $column[$key]['Type'] = $this->clean($item['Type']); + } + + $this->_fields[$table] = array_column($column, 'Default', 'Field'); + + return $column; + } + + /** + * @param $item + * @param $table + */ + private function addPrimary($item, $table) + { + if (!isset($this->_primary[$table])) { + $this->_primary[$table] = []; + } + if ($item['Key'] === 'PRI') { + $this->_primary[$table][] = $item['Field']; + } + $this->addIncrement($item, $table); + } + + + /** + * @param $item + * @param $table + */ + private function addIncrement($item, $table) + { + if ($item['Extra'] !== 'auto_increment') { + return; + } + $this->_auto_increment[$table] = $item['Field']; + } + + + /** + * @param $type + * @return string + */ + public function clean($type): string + { + if (!str_contains($type, ')')) { + return $type; + } + $replace = preg_replace('/\(\d+(,\d+)?\)(\s+\w+)*/', '', $type); + if (str_contains($replace, ' ')) { + $replace = explode(' ', $replace)[1]; + } + return $replace; + } + + /** + * @param null $field + * @return array|string|null + * @throws Exception + */ + public function get_fields($field = null): array|string|null + { + $fields = $this->getAllField(); + if (empty($field)) { + return $fields; + } + if (isset($fields[$field])) { + return strtolower($fields[$field]); + } + return null; + } + + /** + * @return array + * @throws Exception + */ + public function getAllField(): array + { + return $this->columns('Type', 'Field'); + } + +} diff --git a/src/Mysql/Schema.php b/src/Mysql/Schema.php new file mode 100644 index 0000000..9cf42f4 --- /dev/null +++ b/src/Mysql/Schema.php @@ -0,0 +1,36 @@ +_column === null) { + $this->_column = new Columns(['db' => $this->db]); + } + + return $this->_column; + } +} diff --git a/src/Orm/Condition.php b/src/Orm/Condition.php new file mode 100644 index 0000000..41942c4 --- /dev/null +++ b/src/Orm/Condition.php @@ -0,0 +1,29 @@ +where($query); + } +} diff --git a/src/Pagination.php b/src/Pagination.php new file mode 100644 index 0000000..35503fb --- /dev/null +++ b/src/Pagination.php @@ -0,0 +1,200 @@ +activeQuery = $activeQuery; + } + + + public function clean() + { + unset($this->activeQuery, $this->_callback, $this->_group); + $this->_offset = 0; + $this->_limit = 100; + $this->_max = 0; + $this->_length = 0;; + } + + + /** + * recover class by clone + */ + public function __clone() + { + $this->clean(); + } + + + /** + * @param array|Closure $callback + * @throws Exception + */ + public function setCallback(array|Closure $callback) + { + if (!is_callable($callback, true)) { + throw new Exception('非法回调函数~'); + } + $this->_callback = $callback; + } + + + /** + * @param int $number + * @return Pagination + */ + public function setOffset(int $number): static + { + if ($number < 0) { + $number = 0; + } + $this->_offset = $number; + return $this; + } + + + /** + * @param int $number + * @return Pagination + */ + public function setLimit(int $number): static + { + if ($number < 1) { + $number = 100; + } else if ($number > 5000) { + $number = 5000; + } + $this->_limit = $number; + return $this; + } + + + /** + * @param int $number + * @return Pagination + */ + public function setMax(int $number): static + { + if ($number < 0) { + return $this; + } + $this->_max = $number; + return $this; + } + + + /** + * @param array $param + * @return void + * @throws Exception + */ + public function plunk($param = []) + { + $this->loop($param); + } + + + /** + * 轮训 + * @param $param + * @return array + * @throws Exception + */ + public function loop($param): array + { + if ($this->_max > 0 && $this->_length >= $this->_max) { + return $this->output(); + } + [$length, $data] = $this->get(); + + $this->executed($data, $param); + + unset($data); + if ($length < $this->_limit) { + return $this->output(); + } + return $this->loop($param); + } + + + /** + * @return array + */ + public function output(): array + { + return []; + } + + + /** + * @param $data + * @param $param + * @throws Exception + */ + private function executed($data, $param): void + { + try { + call_user_func($this->_callback, $data, $param); + } catch (\Throwable $exception) { + $this->addError($exception, 'throwable'); + } finally { + logger()->insert(); + } + } + + + /** + * @return array|Collection + */ + private function get(): Collection|array + { + if ($this->_max > 0 && $this->_length + $this->_limit > $this->_max) { + $this->_limit = $this->_length + $this->_limit - $this->_max; + } + $data = $this->activeQuery->limit($this->_offset, $this->_limit)->get(); + $this->_offset += $this->_limit; + $this->_length += $data->size(); + return [$data->size(), $data]; + } + +} diff --git a/src/Query.php b/src/Query.php new file mode 100644 index 0000000..dcef535 --- /dev/null +++ b/src/Query.php @@ -0,0 +1,54 @@ +builder = SqlBuilder::builder($this); + } + + /** + * @return string + * @throws Exception + */ + public function getSql(): string + { + return $this->builder->get(); + } + + + /** + * @return string + * @throws Exception + */ + public function getCondition(): string + { + return $this->builder->getCondition(); + } + + +} diff --git a/src/Relation.php b/src/Relation.php new file mode 100644 index 0000000..4296587 --- /dev/null +++ b/src/Relation.php @@ -0,0 +1,111 @@ +_query[$identification] = $query; + return $this; + } + + /** + * @param $name + * @return ActiveQuery|null + */ + public function getQuery(string $name): ?ActiveQuery + { + return $this->_query[$name] ?? null; + } + + + /** + * @param string $identification + * @param $localValue + * @return mixed + * @throws Exception + */ + public function first(string $identification, $localValue): mixed + { + $_identification = $identification . '_first_' . $localValue; + if (isset($this->_relations[$_identification]) && $this->_relations[$_identification] !== null) { + return $this->_relations[$_identification]; + } + + $activeModel = $this->_query[$identification]->first(); + if (empty($activeModel)) { + return null; + } + + return $this->_relations[$_identification] = $activeModel; + } + + + /** + * @param string $identification + * @param $localValue + * @return mixed + * @throws Exception + */ + public function count(string $identification, $localValue): mixed + { + $_identification = $identification . '_count_' . $localValue; + if (isset($this->_relations[$_identification]) && $this->_relations[$_identification] !== null) { + return $this->_relations[$_identification]; + } + + $activeModel = $this->_query[$identification]->count(); + if (empty($activeModel)) { + return null; + } + + return $this->_relations[$_identification] = $activeModel; + } + + + /** + * @param string $identification + * @param $localValue + * @return mixed + */ + public function get(string $identification, $localValue): mixed + { + if (is_array($localValue)) { + $_identification = $identification . '_get_' . implode('_', $localValue); + } else { + $_identification = $identification . '_get_' . $localValue; + } + if (isset($this->_relations[$_identification]) && $this->_relations[$_identification] !== null) { + return $this->_relations[$_identification]; + } + + $activeModel = $this->_query[$identification]->get(); + if (empty($activeModel)) { + return null; + } + + return $this->_relations[$_identification] = $activeModel; + } + +} diff --git a/src/SqlBuilder.php b/src/SqlBuilder.php new file mode 100644 index 0000000..30a9c91 --- /dev/null +++ b/src/SqlBuilder.php @@ -0,0 +1,369 @@ + $query]); + } + + + /** + * @return string + * @throws Exception + */ + public function getCondition(): string + { + return $this->conditionToString(); + } + + + /** + * @param array $compiler + * @return string + * @throws Exception + */ + public function hashCompiler(array $compiler): string + { + return $this->where($compiler); + } + + + /** + * @param array $attributes + * @return bool|array + * @throws Exception + */ + public function update(array $attributes): bool|array + { + [$string, $array] = $this->builderParams($attributes); + + return $this->__updateBuilder($string, $array); + } + + + /** + * @param array $attributes + * @param string $opera + * @return bool|array + * @throws Exception + */ + public function mathematics(array $attributes, string $opera = '+'): bool|array + { + $string = []; + foreach ($attributes as $attribute => $value) { + $string[] = $attribute . '=' . $attribute . $opera . $value; + } + return $this->__updateBuilder($string, []); + } + + + /** + * @param array $string + * @param array $params + * @return array|bool + * @throws Exception + */ + private function __updateBuilder(array $string, array $params): array|bool + { + if (empty($string)) { + return $this->addError('None data update.'); + } + + $condition = $this->conditionToString(); + if (!empty($condition)) { + $condition = ' WHERE ' . $condition; + } + + $update = 'UPDATE ' . $this->tableName() . ' SET ' . implode(',', $string) . $condition; + $update .= $this->builderLimit($this->query, false); + + return [$update, $params]; + } + + + /** + * @param array $attributes + * @param false $isBatch + * @return array + * @throws Exception + */ + public function insert(array $attributes, bool $isBatch = false): array + { + $update = sprintf('INSERT INTO %s', $this->tableName()); + if ($isBatch === false) { + $attributes = [$attributes]; + } + $update .= '(' . implode(',', $this->getFields($attributes)) . ') VALUES '; + + $order = 0; + $keys = $params = []; + foreach ($attributes as $attribute) { + [$_keys, $params] = $this->builderParams($attribute, true, $params, $order); + + $keys[] = implode(',', $_keys); + $order++; + } + return [$update . '(' . implode('),(', $keys) . ')', $params]; + } + + + /** + * @return string + * @throws Exception + */ + public function delete(): string + { + $delete = sprintf('DELETE FROM %s ', $this->query->modelClass::getTable()); + + $this->query->from = null; + + return $delete . ' WHERE ' . $this->_prefix(true); + } + + + /** + * @param $attributes + * @return array + */ + #[Pure] private function getFields($attributes): array + { + return array_keys(current($attributes)); + } + + + /** + * @param array $attributes + * @param bool $isInsert + * @param array $params + * @param int $order + * @return array[] + * a=:b, + */ + #[Pure] private function builderParams(array $attributes, bool $isInsert = false, array $params = [], int $order = 0): array + { + $keys = []; + foreach ($attributes as $key => $value) { + if ($isInsert === true) { + $keys[] = ':' . $key . $order; + $params[$key . $order] = $value; + } else { + [$keys, $params] = $this->resolveParams($key, $value, $order, $params, $keys); + } + } + return [$keys, $params]; + } + + + /** + * @param string $key + * @param mixed $value + * @param int $order + * @param array $params + * @param array $keys + * @return array + */ + private function resolveParams(string $key, mixed $value, int $order, array $params, array $keys): array + { + if ( + str_starts_with($value, '+ ') || + str_starts_with($value, '- ') + ) { + $keys[] = $key . '=' . $key . ' ' . $value; + } else { + $params[$key . $order] = $value; + $keys[] = $key . '=:' . $key . $order; + } + return [$keys, $params]; + } + + + /** + * @return string + * @throws Exception + */ + public function one(): string + { + $this->query->limit(0, 1); + if (empty($this->query->from) && !empty($this->query->modelClass)) { + $this->query->from($this->query->getTable()); + } + return $this->_prefix(true); + } + + + /** + * @return string + * @throws Exception + */ + public function all(): string + { + if (empty($this->query->from) && !empty($this->query->modelClass)) { + $this->query->from($this->query->getTable()); + } + return $this->_prefix(true); + } + + + /** + * @return string + * @throws Exception + */ + public function count(): string + { + if (empty($this->query->from) && !empty($this->query->modelClass)) { + $this->query->from($this->query->getTable()); + } + return $this->_prefix(); + } + + + /** + * @param $table + * @return string + */ + public function columns($table): string + { + return 'SHOW FULL FIELDS FROM ' . $table; + } + + + /** + * @param bool $hasOrder + * @return string + * @throws Exception + */ + private function _prefix(bool $hasOrder = false): string + { + $select = ''; + if (!empty($this->query->from)) { + $select = $this->_selectPrefix(); + } + $select = $this->_wherePrefix($select); + if (!empty($this->query->attributes) && is_array($this->query->attributes)) { + $select = strtr($select, $this->query->attributes); + } + + if (!empty($this->query->group)) { + $select .= $this->builderGroup($this->query->group); + } + if ($hasOrder === true && !empty($this->query->order)) { + $select .= $this->builderOrder($this->query->order); + } + return $select . $this->builderLimit($this->query); + } + + + /** + * @param $select + * @return string + * @throws Exception + */ + private function _wherePrefix($select): string + { + $condition = $this->conditionToString(); + if (empty($condition)) { + return $select; + } else if (empty($select)) { + return $condition; + } + return sprintf('%s WHERE %s', $select, $condition); + } + + + /** + * @return string + * @throws Exception + */ + private function _selectPrefix(): string + { + $select = $this->builderSelect($this->query->select) . ' FROM ' . $this->tableName(); + if (!empty($this->query->alias)) { + $select .= $this->builderAlias($this->query->alias); + } + if (!empty($this->query->join)) { + $select .= $this->builderJoin($this->query->join); + } + return $select; + } + + + /** + * @param false $isCount + * @return string + * @throws Exception + */ + public function get(bool $isCount = false): string + { + if ($isCount === false) { + return $this->all(); + } + return $this->count(); + } + + + /** + * @return string + * @throws Exception + */ + public function truncate(): string + { + return sprintf('TRUNCATE %s', $this->tableName()); + } + + + /** + * @return string + * @throws Exception + */ + private function conditionToString(): string + { + return $this->where($this->query->where); + } + + + /** + * @return string + * @throws Exception + */ + public function tableName(): string + { + if ($this->query->from instanceof \Closure) { + $this->query->from = sprintf('(%s)', $this->query->makeClosureFunction($this->query->from)); + } + if ($this->query->from instanceof ActiveQuery) { + $this->query->from = sprintf('%s', SqlBuilder::builder($this->query->from)->get($this->query->from)); + } + if (empty($this->query->from)) { + return $this->query->modelClass::getTable(); + } + return $this->query->from; + } + +} diff --git a/src/Traits/Builder.php b/src/Traits/Builder.php new file mode 100644 index 0000000..ea7d12e --- /dev/null +++ b/src/Traits/Builder.php @@ -0,0 +1,218 @@ +toSql() . ')'; + } + return " FROM " . $table; + } + + /** + * @param $join + * @return string + */ + #[Pure] private function builderJoin($join): string + { + if (!empty($join)) { + return ' ' . implode(' ', $join); + } + return ''; + } + + + /** + * @param null $select + * @return string + */ + #[Pure] private function builderSelect($select = NULL): string + { + if (empty($select)) { + return "SELECT *"; + } + if (is_array($select)) { + return "SELECT " . implode(',', $select); + } else { + return "SELECT " . $select; + } + } + + + /** + * @param $group + * @return string + */ + private function builderGroup($group): string + { + if (empty($group)) { + return ''; + } + return ' GROUP BY ' . $group; + } + + /** + * @param $order + * @return string + */ + #[Pure] private function builderOrder($order): string + { + if (!empty($order)) { + return ' ORDER BY ' . implode(',', $order); + } else { + return ''; + } + } + + /** + * @param ActiveQuery|Query $query + * @param bool $hasLimit + * @return string + */ + #[Pure] private function builderLimit(ActiveQuery|Query $query, bool $hasLimit = true): string + { + if (!is_numeric($query->limit) || $query->limit < 1) { + return ""; + } + if ($query->offset !== null && $hasLimit) { + return ' LIMIT ' . $query->offset . ',' . $query->limit; + } + return ' LIMIT ' . $query->limit; + } + + + /** + * @param $where + * @return string + * @throws Exception + */ + private function where($where): string + { + $_tmp = []; + if (empty($where)) return ''; + if (is_string($where)) return $where; + foreach ($where as $key => $value) { + $_value = $this->resolveCondition($key, $value, $_tmp); + + if (empty($_value)) continue; + $_tmp[] = $_value; + } + if (!empty($_tmp)) { + return implode(' AND ', $_tmp); + } + return ''; + } + + + /** + * @param $field + * @param $condition + * @param $_tmp + * @return string + * @throws NotFindClassException + * @throws ReflectionException + */ + private function resolveCondition($field, $condition, $_tmp): string + { + if (is_string($field)) { + $_value = sprintf('%s = \'%s\'', $field, $condition); + } else if (is_string($condition)) { + $_value = $condition; + } else { + $_value = $this->_arrayMap($condition, $_tmp); + } + return $_value; + } + + + /** + * @param $condition + * @param $array + * @return string + * @throws NotFindClassException + * @throws ReflectionException + * @throws ReflectionException + */ + private function _arrayMap($condition, $array): string + { + if (!isset($condition[0])) { + return implode(' AND ', $this->_hashMap($condition)); + } + $stroppier = strtoupper($condition[0]); + if (str_contains($stroppier, 'OR')) { + if (!is_string($condition[2])) { + $condition[2] = $this->_hashMap($condition[2]); + } + $builder = Kiri::createObject(['class' => OrCondition::class, 'value' => $condition[2], 'column' => $condition[1], 'oldParams' => $array]); + } else if (isset(ConditionClassMap::$conditionMap[$stroppier])) { + $defaultConfig = ConditionClassMap::$conditionMap[$stroppier]; + $create = array_merge($defaultConfig, ['column' => $condition[1], 'value' => $condition[2]]); + $builder = Kiri::createObject($create); + } else { + $builder = Kiri::createObject(['class' => HashCondition::class, 'value' => $condition]); + } + + $array[] = $builder->builder(); + + return implode(' AND ', $array); + } + + + /** + * @param $condition + * @return array + */ + private function _hashMap($condition): array + { + $_array = []; + foreach ($condition as $key => $value) { + $value = is_numeric($value) ? $value : '\'' . $value . '\''; + if (!is_numeric($key)) { + $_array[] = sprintf('%s = %s', $key, $value); + } else { + $_array[] = $value; + } + } + return $_array; + } + + +} diff --git a/src/Traits/HasBase.php b/src/Traits/HasBase.php new file mode 100644 index 0000000..cad3c09 --- /dev/null +++ b/src/Traits/HasBase.php @@ -0,0 +1,83 @@ +whereIn($primaryId, $value); + } else { + $_model = $model::find()->where(['t1.' . $primaryId => $value]); + } + + $this->_relation = $relation->bindIdentification($model, $_model); + + $this->model = $model; + $this->value = $value; + } + + abstract public function get(); + + /** + * @param $name + * @return mixed + */ + public function __get($name): mixed + { + if (empty($this->value)) { + return null; + } + return $this->get(); + } +} diff --git a/src/Traits/QueryTrait.php b/src/Traits/QueryTrait.php new file mode 100644 index 0000000..9daac69 --- /dev/null +++ b/src/Traits/QueryTrait.php @@ -0,0 +1,938 @@ +where = []; + $this->select = []; + $this->join = []; + $this->order = []; + $this->offset = NULL; + $this->limit = NULL; + $this->group = ''; + $this->from = ''; + $this->alias = 't1'; + $this->filter = []; + } + + + /** + * @param string $column + * @param callable $callable + * @return $this + */ + public function when(string $column, callable $callable): static + { + $caseWhen = new When($column, $this); + + call_user_func($callable, $caseWhen); + + $this->where[] = $caseWhen->end(); + + return $this; + } + + + /** + * @param string $whereRaw + * @return QueryTrait + */ + public function whereRaw(string $whereRaw): static + { + $this->where[] = $whereRaw; + return $this; + } + + + /** + * @param string|array|Closure $condition + * @param string|array|Closure $condition1 + * @param string|array|Closure $condition2 + * @return $this + * @throws NotFindClassException + * @throws ReflectionException + */ + public function whereIf(string|array|Closure $condition, string|array|Closure $condition1, string|array|Closure $condition2): static + { + if (!is_string($condition)) { + $condition = $this->makeClosureFunction($condition); + } + + if (!is_string($condition1)) { + $condition1 = $this->makeClosureFunction($condition1); + } + + if (!is_string($condition2)) { + $condition2 = $this->makeClosureFunction($condition2); + } + + $this->where[] = 'IF(' . $condition . ', ' . $condition1 . ', ' . $condition2 . ')'; + return $this; + } + + + /** + * @param $bool + * @return $this + */ + public function ifNotWhere($bool): static + { + $this->ifNotWhere = $bool; + return $this; + } + + /** + * @return string + * @throws Exception + */ + public function getTable(): string + { + return $this->modelClass::getTable(); + } + + + /** + * @param string $column + * @param string $value + * @return $this + */ + public function whereLocate(string $column, string $value): static + { + $this->where[] = 'LOCATE(' . $column . ',\'' . addslashes($value) . '\') > 0'; + return $this; + } + + + /** + * @param string $column + * @return $this + */ + public function whereNull(string $column): static + { + $this->where[] = $column . ' IS NULL'; + return $this; + } + + + /** + * @param string $column + * @return $this + */ + public function whereEmpty(string $column): static + { + $this->where[] = $column . ' = \'\''; + return $this; + } + + /** + * @param string $column + * @return $this + */ + public function whereNotEmpty(string $column): static + { + $this->where[] = $column . ' <> \'\''; + return $this; + } + + /** + * @param string $column + * @return $this + */ + public function whereNotNull(string $column): static + { + $this->where[] = $column . ' IS NOT NULL'; + return $this; + } + + /** + * @param array|Closure|string $columns + * @return $this + */ + public function filter(array|Closure|string $columns): static + { + if (!$columns) { + return $this; + } + if (is_callable($columns, TRUE)) { + return call_user_func($columns, $this); + } + if (is_string($columns)) { + $columns = explode(',', $columns); + } + if (!is_array($columns)) { + return $this; + } + $this->filter = $columns; + return $this; + } + + /** + * @param string $alias + * + * @return $this + * + * select * from tableName as t1 + */ + public function alias(string $alias = 't1'): static + { + $this->alias = $alias; + return $this; + } + + /** + * @param string|Closure $tableName + * + * @return $this + */ + public function from(string|Closure $tableName): static + { + $this->from = $tableName; + return $this; + } + + /** + * @param string $tableName + * @param string $alias + * @param null $on + * @param array|null $param + * @return $this + * $query->join([$tableName, ['userId'=>'uuvOd']], $param) + * $query->join([$tableName, ['userId'=>'uuvOd'], $param]) + * $query->join($tableName, ['userId'=>'uuvOd',$param]) + */ + private function join(string $tableName, string $alias, $on = NULL, array $param = NULL): static + { + if (empty($on)) { + return $this; + } + $join[] = $tableName . ' AS ' . $alias; + $join[] = 'ON ' . $this->onCondition($alias, $on); + if (empty($join)) { + return $this; + } + + $this->join[] = implode(' ', $join); + if (!empty($param)) { + $this->addParams($param); + } + + return $this; + } + + /** + * @param $alias + * @param $on + * @return string + */ + private function onCondition($alias, $on): string + { + $array = []; + foreach ($on as $key => $item) { + if (!str_contains($item, '.')) { + $this->addParam($key, $item); + } else { + $explode = explode('.', $item); + if (isset($explode[1]) && ($explode[0] == $alias || $this->alias == $explode[0])) { + $array[] = $key . '=' . $item; + } else { + $this->addParam($key, $item); + } + } + } + return implode(' AND ', $array); + } + + /** + * @param string $tableName + * @param string $alias + * @param $onCondition + * @param null $param + * @return $this + * @throws Exception + */ + public function leftJoin(string $tableName, string $alias, $onCondition, $param = NULL): static + { + if (class_exists($tableName)) { + if (!in_array(ActiveRecord::class, class_implements($tableName))) { + throw new Exception('Model must implement ' . $tableName); + } + $tableName = $tableName::getTable(); + } + return $this->join(...["LEFT JOIN " . $tableName, $alias, $onCondition, $param]); + } + + /** + * @param $tableName + * @param $alias + * @param $onCondition + * @param null $param + * @return $this + * @throws Exception + */ + public function rightJoin($tableName, $alias, $onCondition, $param = NULL): static + { + if (class_exists($tableName)) { + if (!in_array(ActiveRecord::class, class_implements($tableName))) { + throw new Exception('Model must implement ' . $tableName); + } + $tableName = $tableName::getTable(); + } + return $this->join(...["RIGHT JOIN " . $tableName, $alias, $onCondition, $param]); + } + + /** + * @param $tableName + * @param $alias + * @param $onCondition + * @param null $param + * @return $this + * @throws Exception + */ + public function innerJoin($tableName, $alias, $onCondition, $param = NULL): static + { + if (class_exists($tableName)) { + if (!in_array(ActiveRecord::class, class_implements($tableName))) { + throw new Exception('Model must implement ' . $tableName); + } + $tableName = $tableName::getTable(); + } + return $this->join(...["INNER JOIN " . $tableName, $alias, $onCondition, $param]); + } + + /** + * @param $array + * + * @return string + */ + private function toString($array): string + { + $tmp = []; + if (!is_array($array)) { + return $array; + } + foreach ($array as $key => $val) { + if (is_array($val)) { + $tmp[] = $this->toString($array); + } else { + $tmp[] = $key . '=:' . $key; + $this->attributes[':' . $key] = $val; + } + } + return implode(' AND ', $tmp); + } + + /** + * @param string $field + * + * @return $this + */ + public function sum(string $field): static + { + $this->select[] = 'SUM(' . $field . ') AS ' . $field; + return $this; + } + + /** + * @param string $field + * @return $this + */ + public function max(string $field): static + { + $this->select[] = 'MAX(' . $field . ') AS ' . $field; + return $this; + } + + /** + * @param string $lngField + * @param string $latField + * @param int $lng1 + * @param int $lat1 + * + * @return $this + */ + public function distance(string $lngField, string $latField, int $lng1, int $lat1): static + { + $sql = "ROUND(6378.138 * 2 * ASIN(SQRT(POW(SIN(($lat1 * PI() / 180 - $lat1 * PI() / 180) / 2),2) + COS($lat1 * PI() / 180) * COS($latField * PI() / 180) * POW(SIN(($lng1 * PI() / 180 - $lngField * PI() / 180) / 2),2))) * 1000) AS distance"; + $this->select[] = $sql; + return $this; + } + + /** + * @param string|array $column + * @param string $sort + * + * @return $this + * + * [ + * 'addTime', + * 'descTime desc' + * ] + */ + public function orderBy(string|array $column, string $sort = 'DESC'): static + { + if (empty($column)) { + return $this; + } + if (is_string($column)) { + return $this->addOrder(...func_get_args()); + } + + foreach ($column as $key => $val) { + $this->addOrder($val); + } + + return $this; + } + + /** + * @param string $column + * @param string $sort + * + * @return $this + * + */ + private function addOrder(string $column, string $sort = 'DESC'): static + { + $column = trim($column); + + if (func_num_args() == 1 || str_contains($column, ' ')) { + $this->order[] = $column; + } else { + $this->order[] = "$column $sort"; + } + return $this; + } + + /** + * @param array|string $column + * + * @return $this + */ + public function select(array|string $column = '*'): static + { + if ($column == '*') { + $this->select = $column; + } else { + if (!is_array($column)) { + $column = explode(',', $column); + } + foreach ($column as $val) { + $this->select[] = $val; + } + } + return $this; + } + + /** + * @return $this + */ + public function orderRand(): static + { + $this->order[] = 'RAND()'; + return $this; + } + + /** + * @param array|Closure|string $conditionArray + * @param string $opera + * @param null $value + * @return QueryTrait + * @throws NotFindClassException + * @throws ReflectionException + */ + public function whereOr(array|Closure|string $conditionArray = [], string $opera = '=', $value = null): static + { + if ($conditionArray instanceof Closure) { + $conditionArray = $this->makeClosureFunction($conditionArray); + } + + if (func_num_args() > 1) { + [$conditionArray, $opera, $value] = $this->opera(...func_get_args()); + + $conditionArray = $this->sprintf($conditionArray, $value, $opera); + } + + $this->where = ['(' . implode(' AND ', $this->where) . ') OR (' . $conditionArray . ')']; + return $this; + } + + /** + * @param string $columns + * @param string|int|bool|null $value + * + * @param string $opera + * @return QueryTrait + */ + public function whereAnd(string $columns, string $opera = '=', string|int|null|bool $value = NULL): static + { + [$columns, $opera, $value] = $this->opera(...func_get_args()); + + $this->where[] = $this->sprintf($columns, $value, $opera); + return $this; + } + + + /** + * @param string $columns + * @param string $value + * @return $this + * @throws Exception + */ + public function whereLike(string $columns, string $value): static + { + if (empty($columns) || empty($value)) { + return $this; + } + + if (is_array($columns)) { + $columns = 'CONCAT(' . implode(',^,', $columns) . ')'; + } + + $this->where[] = $columns . ' LIKE \'%' . addslashes($value) . '%\''; + + return $this; + } + + /** + * @param string $columns + * @param string $value + * @return $this + * @throws Exception + */ + public function whereLeftLike(string $columns, string $value): static + { + if (empty($columns) || empty($value)) { + return $this; + } + + if (is_array($columns)) { + $columns = 'CONCAT(' . implode(',^,', $columns) . ')'; + } + + $this->where[] = $columns . ' LLike \'%' . addslashes($value) . '\''; + + return $this; + } + + /** + * @param string $columns + * @param string $value + * @return $this + * @throws Exception + */ + public function whereRightLike(string $columns, string $value): static + { + if (empty($columns) || empty($value)) { + return $this; + } + + if (is_array($columns)) { + $columns = 'CONCAT(' . implode(',^,', $columns) . ')'; + } + + $this->where[] = $columns . ' RLike \'' . addslashes($value) . '%\''; + + return $this; + } + + + /** + * @param string $columns + * @param string $value + * @return $this + * @throws Exception + */ + public function whereNotLike(string $columns, string $value): static + { + if (empty($columns) || empty($value)) { + return $this; + } + + if (is_array($columns)) { + $columns = 'CONCAT(' . implode(',^,', $columns) . ')'; + } + + $this->where[] = $columns . ' NOT LIKE \'%' . addslashes($value) . '%\''; + + return $this; + } + + /** + * @param string $column + * @param int $value + * @return $this + * @throws Exception + * @see MathematicsCondition + */ + public function whereEq(string $column, int $value): static + { + $this->where[] = ['EQ', $column, $value]; + + return $this; + } + + + /** + * @param string $column + * @param int $value + * @return $this + * @throws Exception + * @see MathematicsCondition + */ + public function whereNeq(string $column, int $value): static + { + $this->where[] = ['NEQ', $column, $value]; + + return $this; + } + + + /** + * @param string $column + * @param int $value + * @return $this + * @throws Exception + * @see MathematicsCondition + */ + public function whereGt(string $column, int $value): static + { + $this->where[] = ['GT', $column, $value]; + + return $this; + } + + /** + * @param string $column + * @param int $value + * @return $this + * @throws Exception + * @see MathematicsCondition + */ + public function whereEgt(string $column, int $value): static + { + $this->where[] = ['EGT', $column, $value]; + + return $this; + } + + + /** + * @param string $column + * @param int $value + * @return $this + * @throws Exception + * @see MathematicsCondition + */ + public function whereLt(string $column, int $value): static + { + $this->where[] = ['LT', $column, $value]; + + return $this; + } + + /** + * @param string $column + * @param int $value + * @return $this + * @throws Exception + * @see MathematicsCondition + */ + public function whereElt(string $column, int $value): static + { + $this->where[] = ['ELT', $column, $value]; + + return $this; + } + + /** + * @param string $columns + * @param array|Closure $value + * @return $this + * @throws NotFindClassException + * @throws ReflectionException + */ + public function whereIn(string $columns, array|Closure $value): static + { + if ($value instanceof Closure) { + $value = $this->makeClosureFunction($value); + } + if (empty($value)) { + $value = [-1]; + } + $this->where[] = ['IN', $columns, $value]; + return $this; + } + + + /** + * @param $value + * @return ActiveQuery + * @throws Exception + */ + public function makeNewQuery($value): ActiveQuery + { + $activeQuery = new ActiveQuery($this->modelClass); + call_user_func($value, $activeQuery); + if (empty($activeQuery->from)) { + $activeQuery->from($activeQuery->modelClass::getTable()); + } + return $activeQuery; + } + + + /** + * @return Query + * @throws ReflectionException + * @throws NotFindClassException + */ + public function makeNewSqlGenerate(): Query + { + return Kiri::createObject(['class' => Query::class]); + } + + + /** + * @param string $columns + * @param array $value + * @return $this + */ + public function whereNotIn(string $columns, array $value): static + { + if (empty($value) || !is_array($value)) { + $value = [-1]; + } + $this->where[] = ['NOT IN', $columns, $value]; + return $this; + } + + /** + * @param string $column + * @param int $start + * @param int $end + * @return $this + */ + public function whereBetween(string $column, int $start, int $end): static + { + if (empty($column) || empty($start) || empty($end)) { + return $this; + } + + $this->where[] = $column . ' BETWEEN ' . $start . ' AND ' . $end; + + return $this; + } + + /** + * @param string $column + * @param int $start + * @param int $end + * @return $this + */ + public function whereNotBetween(string $column, int $start, int $end): static + { + if (empty($column) || empty($start) || empty($end)) { + return $this; + } + + $this->where[] = $column . 'NOT BETWEEN' . $start . ' AND ' . $end; + + return $this; + } + + /** + * @param array $params + * + * @return $this + */ + public function bindParams(array $params = []): static + { + if (empty($params)) { + return $this; + } + $this->attributes = $params; + return $this; + } + + /** + * @param Closure|array|string $column + * @param string $opera + * @param null $value + * @return $this + * @throws NotFindClassException + * @throws ReflectionException + */ + public function where(Closure|array|string $column, string $opera = '=', $value = null): static + { + if (is_array($column)) { + return $this->addArray($column); + } + if ($column instanceof Closure) { + $this->where[] = $this->makeClosureFunction($column); + return $this; + } + [$column, $opera, $value] = $this->opera(...func_get_args()); + $this->where[] = "$column $opera $value"; + return $this; + } + + + /** + * @param Closure|array $closure + * @return string + * @throws NotFindClassException + * @throws ReflectionException + * @throws Exception + */ + public function makeClosureFunction(Closure|array $closure): string + { + $generate = $this->makeNewSqlGenerate(); + if ($closure instanceof Closure) { + call_user_func($closure, $generate); + } else { + $generate->where($closure); + } + return $generate->getSql(); + } + + + /** + * @param string $name + * @param string|null $having + * + * @return $this + */ + public function groupBy(string $name, string $having = NULL): static + { + $this->group = $name; + if (empty($having)) { + return $this; + } + $this->group .= ' HAVING ' . $having; + return $this; + } + + /** + * @param int $offset + * @param int $limit + * + * @return $this + */ + public function limit(int $offset, int $limit = 20): static + { + $this->offset = $offset; + $this->limit = $limit; + return $this; + } + + + /** + * @return array + */ + private function opera(): array + { + if (func_num_args() == 3) { + [$column, $opera, $value] = func_get_args(); + } else { + [$column, $value] = func_get_args(); + } + if (!isset($opera)) { + $opera = '='; + } + return [$column, $opera, $value]; + } + + + /** + * @param array $array + * @return $this + */ + private function addArray(array $array): static + { + foreach ($array as $key => $value) { + if (is_numeric($key)) { + [$column, $opera, $value] = $this->opera(...$value); + + $this->where[] = $this->sprintf($column, $value, $opera); + } else { + $this->where[] = $this->sprintf($key, $value); + } + } + return $this; + } + + + /** + * @param $column + * @param $value + * @param string $opera + * @return string + */ + private function sprintf($column, $value, string $opera = '='): string + { + if (is_string($value)) { + $value = trim($value, '\'"'); + } + return "$column $opera '$value'"; + } + + + /** + * @return $this + */ + public function oneLimit(): static + { + $this->limit = 1; + return $this; + } + +} diff --git a/src/Traits/When.php b/src/Traits/When.php new file mode 100644 index 0000000..35964d8 --- /dev/null +++ b/src/Traits/When.php @@ -0,0 +1,77 @@ +_condition[] = 'CASE ' . $column; + } + + + /** + * @param string|int $condition + * @param string $then + * @return $this + * @throws Exception + */ + public function when(string|int $condition, string $then): static + { + $this->_condition[] = sprintf('WHEN %s THEN %s', $condition, $then); + + return $this; + } + + + /** + * @param string $alias + */ + public function else(string $alias) + { + $this->else = $alias; + } + + + /** + * @return string + */ + #[Pure] public function end(): string + { + if (empty($this->_condition)) { + return ''; + } + $prefix = implode(' ', $this->_condition); + if (!empty($this->else)) { + $prefix .= ' ELSE ' . $this->else; + } + return '(' . $prefix . ' END)'; + } + +}