This commit is contained in:
gaofeng
2026-05-12 18:27:28 +08:00
commit 6d9aee81aa
3664 changed files with 274415 additions and 0 deletions

View File

@@ -0,0 +1,719 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\concern;
use BackedEnum;
use Stringable;
use think\db\Express;
use think\db\Raw;
use think\helper\Str;
use think\model\Collection;
use think\model\contract\EnumTransform;
use think\model\contract\FieldTypeTransform;
use think\model\contract\Modelable as Model;
use think\model\contract\Typeable;
use think\model\type\Date;
use think\model\type\DateTime;
use think\model\type\Json;
/**
* 模型数据处理.
*/
trait Attribute
{
/**
* 初始化模型数据.
*
* @param array|object $data 实体模型数据
* @param bool $fromSave
*
* @return void
*/
private function initializeData(array | object $data, bool $fromSave = false)
{
// 分析数据
$data = $this->parseData($data);
$schema = $this->getFields();
$fields = array_keys($schema);
// 模型赋值
foreach ($data as $name => $value) {
if (in_array($name, $this->getOption('disuse'))) {
// 废弃字段
continue;
}
if (str_contains($name, '__')) {
// 组装关联JOIN查询数据
[$relation, $attr] = explode('__', $name, 2);
$relations[$relation][$attr] = $value;
continue;
}
$trueName = $fromSave ? $this->getMappingName($name) : $name;
if (in_array($trueName, $fields)) {
$type = $schema[$trueName] ?? 'string';
// 读取数据后进行类型转换
if (!$fromSave || !$this->hasSetAttr($trueName)) {
$value = $this->readTransform($value, $type);
}
// 数据赋值
$this->setData($trueName, $value);
if ($trueName == $this->getPk()) {
$this->setKey($value);
}
// 记录原始数据
$origin[$trueName] = $value;
} else {
// 非数据表字段或关联数据 额外赋值
$this->setData($trueName, $value);
}
}
if (!empty($relations)) {
// 设置关联数据
$this->parseRelationData($relations);
}
if (!empty($origin) && !$fromSave) {
$this->trigger('AfterRead');
$this->setOption('origin', $origin);
$this->setOption('get', []);
}
}
/**
* 获取主键名.
*
* @return string|array
*/
public function getPk()
{
return $this->getOption('pk', 'id');
}
/**
* 获取表名(不含前后缀).
*
* @return string
*/
public function getName(): string
{
return $this->getOption('name', Str::snake(class_basename(static::class)));
}
/**
* 解析模型数据.
*
* @param array|object $data 数据
*
* @return array
*/
private function parseData(array | object $data): array
{
if ($data instanceof self) {
$data = $data->getData();
} elseif (is_object($data)) {
$data = get_object_vars($data);
}
return $data;
}
/**
* 动态设置数据字段获取器.
*
* @param array|string $attr 字段名
* @param callable $callback 闭包获取器
*
* @return $this
*/
public function withFieldAttr(array | string $attr, ?callable $callback = null)
{
if (is_array($attr)) {
foreach ($attr as $name => $closure) {
$this->withFieldAttr($name, $closure);
}
} else {
$name = $this->getRealFieldName($attr);
$this->setWeakData('withAttr', $name, $callback);
// 自动追加输出
self::$weakMap[$this]['append'][] = $name;
}
return $this;
}
/**
* 获取实际字段名.
* 严格模式下 完全和数据表字段对应一致(默认)
* 非严格模式 统一转换为snake规范支持驼峰规范读取
*
* @param string $name 字段名
*
* @return mixed
*/
protected function getRealFieldName(string $name)
{
if (false === $this->getOption('strict')) {
return Str::snake($name);
}
return $name;
}
/**
* 数据读取 类型转换.
*
* @param mixed $value 值
* @param string|array|null $type 要转换的类型
*
* @return mixed
*/
protected function readTransform($value, string | array | null $type)
{
if (is_null($type) || is_null($value) || $value instanceof Raw || $value instanceof Express) {
return $value;
}
$param = '';
if (is_array($type)) {
[$type, $param] = $type;
} elseif (str_contains($type, ':')) {
[$type, $param] = explode(':', $type, 2);
}
$typeTransform = static function (string $type, $value, $model, $param) {
if (class_exists($type) && !($value instanceof $type)) {
if (is_subclass_of($type, Typeable::class)) {
$value = $type::from($value, $model);
if ($param && $value instanceof DateTime) {
// 设置时间输出格式
$value->setFormat($param);
}
} elseif (is_subclass_of($type, FieldTypeTransform::class)) {
$value = $type::get($value, $model);
} elseif (is_subclass_of($type, BackedEnum::class)) {
$value = $type::from($value);
if (is_subclass_of($type, EnumTransform::class)) {
$value = $value->value();
} elseif ($model->getOption('enumReadName')) {
$method = $model->getOption('enumReadName');
$value = is_string($method) ? $value->$method() : $value->name;
}
} else {
// 对象类型
$value = new $type($value);
}
}
return $value;
};
return match ($type) {
'string','bigint'=> (string) $value,
'int','integer' => (int) $value,
'float' => empty($param) ? (float) $value : (float) number_format($value, (int) $param, '.', ''),
'bool','boolean' => (bool) $value,
'array' => empty($value) ? [] : (is_array($value) ? $value : json_decode($value, true)),
'object' => empty($value) ? new \stdClass() : (is_string($value) ? json_decode($value) : json_decode(json_encode($value, JSON_FORCE_OBJECT))),
'json' => $typeTransform(Json::class, $value, $this, $param),
'date' => $typeTransform(Date::class, $value, $this, $param),
'datetime' => $typeTransform(DateTime::class, $value, $this, $param),
'timestamp' => $typeTransform(DateTime::class, $value, $this, $param),
default => $typeTransform($type, $value, $this, $param),
};
}
/**
* 数据写入 类型转换.
*
* @param mixed $value 值
* @param string|array|null $type 要转换的类型
*
* @return mixed
*/
protected function writeTransform($value, string | array | null $type)
{
if (is_null($type) || is_null($value) || $value instanceof Raw || $value instanceof Express) {
return $value;
}
$param = '';
if (is_array($type)) {
[$type, $param] = $type;
} elseif (str_contains($type, ':')) {
[$type, $param] = explode(':', $type, 2);
}
$typeTransform = static function (string $type, $value, $model) {
if (class_exists($type)) {
if (is_subclass_of($type, Typeable::class)) {
$value = $value->value();
} elseif (is_subclass_of($type, FieldTypeTransform::class)) {
$value = $type::set($value, $model);
} elseif ($value instanceof BackedEnum) {
$value = $value->value;
} elseif ($value instanceof Stringable) {
$value = $value->__toString();
}
}
return $value;
};
return match ($type) {
'string','bigint' => (string) $value,
'int', 'integer' => (int) $value,
'float' => empty($param) ? (float) $value : (float) number_format($value, (int) $param, '.', ''),
'bool', 'boolean' => $value ? 1 : 0,
'object' => is_object($value) ? json_encode($value, JSON_FORCE_OBJECT) : $value,
'array' => json_encode((array) $value, JSON_UNESCAPED_UNICODE),
'json' => $typeTransform(Json::class, $value, $this),
'date' => $typeTransform(Date::class, $value, $this),
'datetime' => $typeTransform(DateTime::class, $value, $this),
'timestamp' => $typeTransform(DateTime::class, $value, $this),
default => $typeTransform($type, $value, $this),
};
}
/**
* 刷新对象原始数据(为当前数据).
*
* @return $this
*/
public function refreshOrigin()
{
return $this->setOption('origin', $this->getData());
}
/**
* 设置主键值
*
* @param int|string $value 值
* @return void
*/
public function setKey($value)
{
$pk = $this->getPk();
if (is_string($pk)) {
$this->set($pk, $value);
}
}
/**
* 获取主键值
*
* @return mixed
*/
public function getKey()
{
$pk = $this->getPk();
if (is_null($pk)) {
return;
}
if (is_string($pk)) {
return $this->get($pk);
}
foreach ($pk as $name) {
$data[$name] = $this->get($name);
}
return $data;
}
/**
* 重置模型数据.
*
* @param array $data
*
* @return $this
*/
public function data(array $data)
{
$this->initializeData($data);
return $this;
}
/**
* 获取模型实际数据.
*
* @param string|null $name 字段名
* @return mixed
*/
public function getData(?string $name = null)
{
if ($name) {
$name = $this->getRealFieldName($name);
return $this->getWeakData('data', $name);
}
return $this->getOption('data', []);
}
/**
* 判断模型是否存在数据字段.
*
* @param string $name 字段名
* @return bool
*/
public function hasData(string $name): bool
{
return $this->hasGetAttr($name) || array_key_exists($this->getMappingName($name), self::$weakMap[$this]['data']);
}
/**
* 设置数据对象的实际值
*
* @param string $name 名称
* @param mixed $value 值
*
* @return void
*/
protected function setData(string $name, $value)
{
$this->setWeakData('data', $name, $value);
if ($this->getWeakData('get', $name)) {
$this->setWeakData('get', $name, null);
}
}
/**
* 清空模型数据.
*
* @return $this
*/
public function clear()
{
$this->setOption('data', []);
$this->setOption('origin', []);
$this->setOption('get', []);
$this->setOption('relation', []);
return $this;
}
/**
* 获取原始数据.
*
* @param string|null $name 字段名
* @param bool $transform 是否自动类型转换
* @return mixed
*/
public function getOrigin(?string $name = null, bool $transfrom = false)
{
if ($name) {
$name = $this->getRealFieldName($name);
$result = $this->getWeakData('origin', $name);
return $transfrom ? $this->writeTransform($result, $this->getFields($name)) : $result;
}
return $this->getOption('origin');
}
/**
* 判断数据是否为空.
*
* @return bool
*/
public function isEmpty(): bool
{
return empty($this->getData());
}
/**
* 判断JSON数据是否为数组格式.
*
* @return bool|null
*/
public function isJsonAssoc(): bool|null
{
return $this->getOption('jsonAssoc', true);
}
/**
* 设置JSON数据格式.
*
* @return $this
*/
public function jsonAssoc(bool $assoc = true)
{
return $this->setOption('jsonAssoc', $assoc);
}
/**
* 设置数据对象的值 并进行类型自动转换
*
* @param string $name 名称
* @param mixed $value 值
*
* @return $this
*/
public function set(string $name, $value)
{
$name = $this->getMappingName($name);
$type = $this->getFields()[$name] ?? '';
if ($this->isExists() && in_array($name, $this->getOption('readonly'))) {
// 只读属性不能赋值
return $this;
}
if (is_null($value) && is_subclass_of($type, Model::class)) {
// 关联数据为空 设置一个空模型
$value = new $type();
} elseif (!($value instanceof Model || $value instanceof Collection || $value instanceof FieldTypeTransform) && $type && !$this->hasSetAttr($name)) {
// 类型自动转换
$value = $this->readTransform($value, $type);
}
$this->setData($name, $value);
return $this;
}
/**
* 字段是否定义修改器
*
* @param string $name 名称
*
* @return bool
*/
protected function hasSetAttr(string $name): bool
{
$attr = Str::studly($name);
$method = 'set' . $attr . 'Attr';
return method_exists($this, $method);
}
/**
* 字段是否定义获取器
*
* @param string $name 名称
*
* @return bool
*/
protected function hasGetAttr(string $name): bool
{
$attr = Str::studly($name);
$method = 'get' . $attr . 'Attr';
return method_exists($this, $method);
}
/**
* 使用修改器或类型自动转换处理数据(写入数据前自动调用)
*
* @param string $name 名称
* @param mixed $value 值
*
* @return mixed
*/
private function setWithAttr(string $name, $value)
{
$attr = Str::studly($name);
$method = 'set' . $attr . 'Attr';
if (method_exists($this, $method)) {
$value = $this->$method($value, $this->getData());
} else {
// 类型转换
$value = $this->writeTransform($value, $this->getFields($name));
}
if ($value instanceof Express) {
// 处理运算表达式
$step = $value->getStep();
$origin = $this->getOrigin($name);
$real = match ($value->getType()) {
'+' => $origin + $step,
'-' => $origin - $step,
'*' => $origin * $step,
'/' => $origin / $step,
default => $origin,
};
$this->set($name, $real);
} elseif (is_scalar($value)) {
// 同步写入修改器或类型自动转换结果
$this->set($name, $value);
}
return $value;
}
/**
* 获取数据对象的值(支持使用获取器)
*
* @param string $name 名称
* @param bool $attr 是否使用获取器
*
* @return mixed
*/
public function get(string $name, bool $attr = true)
{
$name = $this->getMappingName($name);
if ($attr && $value = $this->getWeakData('get', $name)) {
// 已经输出的数据直接返回
return $value;
}
if (!array_key_exists($name, $this->getData()) && !array_key_exists($name, $this->getFields())) {
// 动态获取关联数据
$value = $this->getRelationData($name) ?: null;
} else {
$value = $this->getData($name);
}
if ($attr) {
// 通过获取器输出
$value = $this->getWithAttr($name, $value, $this->getData());
$this->setWeakData('get', $name, $value);
}
return $value;
}
/**
* 获取映射字段
*
* @param string $name 名称
*
* @return string
*/
protected function getMappingName(string $name): string
{
$mapping = $this->getOption('mapping');
return array_search($name, $mapping) ?: $this->getRealFieldName($name);
}
/**
* 处理数据对象的值(经过获取器和类型转换)
*
* @param string $name 名称
* @param mixed $value 值
* @param array $data 所有数据
*
* @return mixed
*/
private function getWithAttr(string $name, $value, array $data = [])
{
$attr = Str::studly($name);
$method = 'get' . $attr . 'Attr';
$withAttr = $this->getWeakData('withAttr', $name);
if ($withAttr) {
// 动态获取器
$value = $withAttr($value, $data, $this);
} elseif (method_exists($this, $method)) {
// 获取器
$value = $this->$method($value, $data);
} elseif ($value instanceof Typeable || is_subclass_of($value, EnumTransform::class, false)) {
// 类型自动转换
if ($value instanceof Json) {
// JSON数据转换
$value = $this->readTransformJson($name, $value);
} else {
$value = $value->value();
}
} elseif (is_int($value) && $this->isTimeAttr($name) && false != $this->getDateFormat()) {
// 兼容数字类型时间字段的自动转换输出
$value = (new \DateTime())
->setTimestamp($value)
->format($this->getDateFormat());
}
return $value;
}
/**
* 处理JSON数据对象的值
*
* @param string $name 名称
* @param Json $value 值
*
* @return array|object
*/
protected function readTransformJson(string $name, Json $value)
{
// JSON数据转换
$value = $value->value();
if ($value) {
foreach ($value as $key => &$val) {
$type = $this->getFields($name . '->' . $key);
if ($type) {
// 定义了JSON属性类型自动转换
$val = $this->readTransform($val, $type);
}
}
}
return $value;
}
protected function isTimeAttr(string $name): bool
{
return in_array($name, [$this->getOption('createTime'), $this->getOption('updateTime'), $this->getOption('deleteTime')]) || in_array($name, $this->getOption('timestampField', []));
}
/**
* 使用获取器获取数据对象的值
*
* @param string $name 名称
*
* @return mixed
*/
public function getAttr(string $name)
{
return $this->get($name);
}
/**
* 设置数据对象的值 并进行类型自动转换
*
* @param string $name 名称
* @param mixed $value 值
*
* @return $this
*/
public function setAttr(string $name, $value)
{
return $this->set($name, $value);
}
/**
* 设置数据是否存在.
*
* @param bool $exists
*
* @return $this
*/
public function exists(bool $exists = true)
{
return $this->setOption('exists', $exists);
}
/**
* 判断数据是否存在数据库.
*
* @return bool
*/
public function isExists(): bool
{
return $this->getOption('exists', false);
}
/**
* 设置枚举类型自动读取数据方式
* true 表示使用name值返回
* 字符串 表示使用枚举类的方法返回
*
* @return $this
*/
public function withEnumRead(bool | string $method = true)
{
return $this->setOption('enumReadName', $method);
}
}