720 lines
21 KiB
PHP
720 lines
21 KiB
PHP
<?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);
|
||
}
|
||
}
|