getSetter(static::class, $name); if (!empty($method)) { $value = $this->{$method}($value); } $this->_attributes[$name] = $value; } /** * @param string $name * @param $value * @return mixed * @throws NotFindClassException * @throws ReflectionException */ private function _getter(string $name, $value): mixed { $data = di(Getter::class)->getGetter(static::class, $name); if (empty($data)) { return $this->_relater($name, $value); } return $this->{$data}($value); } /** * @param string $name * @param $value * @return mixed * @throws NotFindClassException * @throws ReflectionException * @throws Exception */ private function _relater(string $name, $value): mixed { $data = di(Relate::class)->getRelate(static::class, $name); if (!empty($data)) { $data = $this->{$data}(); if ($data instanceof HasBase) { return $data->get(); } return $data; } return $this->_decode($name, $value); } /** * @return EventDispatch */ protected function getEventDispatch(): EventDispatch { return Kiri::getDi()->get(EventDispatch::class); } /** * @param $data * @return ModelInterface */ 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 string * @throws Exception * get last exception or other error */ public function getLastError(): string { return Kiri::app()->getLogger()->getLastError('mysql'); } /** * @return bool * @throws Exception */ public function hasPrimary(): bool { if ($this->primary !== NULL) { return true; } $primary = $this->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 $this->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 Model|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::query()->where($param)->first(); } /** * @param $param * @return array * @throws Exception */ private static function getPrimaryCondition($param): array { $primary = static::makeNewInstance()->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 ModelInterface|null * @throws Exception * @throws Exception */ public static function max($field = null): ?ModelInterface { $columns = static::makeNewInstance()->getColumns(); if (empty($field)) { $field = $columns->getFirstPrimary(); } $columns = $columns->get_fields(); if (!isset($columns[$field])) { return null; } $first = static::query()->max($field)->first(); if (empty($first)) { return null; } return $first[$field]; } /** * @param string|int $param * @return Model|null * @throws NotFindClassException * @throws ReflectionException * @throws Exception */ public static function find(string|int $param): ?static { $columns = duplicate(static::class)->getPrimary(); if (empty($columns)) { $columns = static::makeNewInstance()->getColumns()->getFirstPrimary(); } return static::query()->where([$columns => $param])->first(); } /** * @return static */ private static function makeNewInstance(): static { return duplicate(static::class); } /** * @param $condition * @return static|null * @throws NotFindClassException * @throws ReflectionException * @throws Exception */ public static function first($condition): ?static { return static::query()->where($condition)->first(); } /** * @return ActiveQuery */ public static function query(): ActiveQuery { return new ActiveQuery(new static()); } /** * @return Connection */ public function getConnection(): Connection { return Kiri::app()->get('db')->get($this->connection); } /** * @param null $condition * @param array $attributes * * @param bool $if_condition_is_null * @return bool * @throws Exception */ protected 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::query()->delete(); } $model = static::query()->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::query())->insert($param); $dbConnection = $this->getConnection()->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::query()->where($condition))->update($param); if (is_bool($generate)) { return $generate; } $command = $this->getConnection()->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 di(Relate::class)->getRelate(static::class, $name); } /** * @param $attribute * @return bool * @throws Exception */ public function has($attribute): bool { return static::makeNewInstance()->getColumns()->hasField($attribute); } /**ƒ * @return string * @throws Exception */ public function getTable(): string { $connection = static::getConnection(); $tablePrefix = $connection->tablePrefix; if (empty($this->table)) { throw new Exception('You need add static method `tableName` and return table name.'); } $table = trim($this->table, '{{%}}'); if (!empty($tablePrefix) && !str_starts_with($this->table, $tablePrefix)) { $table = $tablePrefix . $this->table; } return '`' . $connection->database . '`.' . $table; } /** * @param $attributes * @param $changeAttributes * @return bool * @throws Exception */ public function afterSave($attributes, $changeAttributes): bool { return true; } /** * @param $model * @return bool */ public function beforeSave($model): bool { return true; } /** * @return array */ public function rules(): array { return []; } /** * @param string $column * @param int $value * @return ModelInterface|false * @throws Exception */ public function increment(string $column, int $value): bool|ModelInterface { if (!$this->mathematics([$column => $value], '+')) { return false; } $this->{$column} += $value; return $this->refresh(); } /** * @param string $column * @param int $value * @return ModelInterface|false * @throws Exception */ public function decrement(string $column, int $value): bool|ModelInterface { if (!$this->mathematics([$column => $value], '-')) { return false; } $this->{$column} -= $value; return $this->refresh(); } /** * @param array $columns * @return ModelInterface|false * @throws Exception */ public function increments(array $columns): bool|static { if (!$this->mathematics($columns, '+')) { return false; } foreach ($columns as $key => $attribute) { $this->$key += $attribute; } return $this; } /** * @param array $columns * @return ModelInterface|false * @throws Exception */ public function decrements(array $columns): bool|static { if (!$this->mathematics($columns, '-')) { return false; } foreach ($columns as $key => $attribute) { $this->$key -= $attribute; } return $this; } /** * @param array $condition * @param array $attributes * @return bool|ModelInterface * @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::query()->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::query()->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 = null): int|bool|array|string|null { if (empty($condition)) { $condition = [$this->getPrimary() => $this->getPrimaryValue()]; } $activeQuery = static::query()->where($condition); $create = SqlBuilder::builder($activeQuery)->mathematics($columns, $action); if (is_bool($create)) { return false; } return $this->getConnection()->createCommand($create[0], $create[1])->exec(); } /** * @param array $fields * @return ModelInterface|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 { $result = false; if (empty($data)) { error('Insert data empty.', 'mysql'); } else { $result = static::query()->batchInsert($data); } return $result; } /** * @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::query()->where($condition); return $condition->batchUpdate($attributes); } /** * @param $condition * @return array|Collection * @throws NotFindClassException * @throws ReflectionException */ public static function get($condition): Collection|array { return static::query()->where($condition)->all(); } /** * @param $condition * @param array $attributes * * @return array|Collection * @throws Exception */ public static function findAll($condition, array $attributes = []): array|Collection { $query = static::query()->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 ToArray) { 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 = di(Getter::class)->getGetter(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; } /** * @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 { $this->_setter($name, $value); } } /** * @param $name * @return mixed * @throws Exception */ public function __get($name): mixed { $method = 'get' . ucfirst($name); if (method_exists($this, $method)) { return $this->{$method}(); } $value = $this->_attributes[$name] ?? null; return $this->_getter($name, $value); } /** * @param $name * @param $value * @return mixed * @throws Exception */ private function _decode($name, $value): mixed { return $this->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); } /** * @return Columns * @throws Exception */ public function getColumns(): Columns { return $this->getConnection()->getSchema()->getColumns() ->table($this->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; } /** * @param $method * @param $value * @return Closure */ protected function dispatcher($method, $value): Closure { return function () use ($method, $value) { return $this->{$method}($value); }; } /** * @param string $name * @param array $arguments * @return mixed */ public static function __callStatic(string $name, array $arguments) { return (new static())->{$name}(...$arguments); } }