// +---------------------------------------------------------------------- declare (strict_types = 1); namespace think\model\concern; use BackedEnum; use Closure; use InvalidArgumentException; use Stringable; use think\db\Raw; use think\helper\Str; use think\Model; use think\model\contract\EnumTransform; use think\model\contract\FieldTypeTransform; use think\model\Relation; /** * 模型数据处理. */ trait Attribute { /** * 数据表主键 复合主键使用数组定义. * * @var string|array */ protected $pk = 'id'; /** * 数据表主键自增. * * @var bool|null|string */ protected $autoInc; /** * 数据表字段信息 留空则自动获取. * * @var array */ protected $schema = []; /** * 当前允许写入的字段. * * @var array */ protected $field = []; /** * 字段自动类型转换. * * @var array */ protected $type = []; /** * 数据表废弃字段. * * @var array */ protected $disuse = []; /** * 数据表只读字段. * * @var array */ protected $readonly = []; /** * 当前模型数据. * * @var array */ private $data = []; /** * 原始数据. * * @var array */ private $origin = []; /** * JSON数据表字段. * * @var array */ protected $json = []; /** * JSON数据表字段类型. * * @var array */ protected $jsonType = []; /** * JSON数据取出是否需要转换为数组. * * @var bool */ protected $jsonAssoc = false; /** * Enum数据取出自动转换为name. * * @var bool|string */ protected $enumReadName = false; /** * 严格检查Enum数据类型. * * @var bool */ protected $enumStrict = false; /** * 是否严格字段大小写. * * @var bool */ protected $strict = true; /** * 获取器数据. * * @var array */ private $get = []; /** * 动态获取器. * * @var array */ private $withAttr = []; /** * 自动写入字段. * * @var array */ protected $insert = []; /** * 获取模型对象的主键. * * @return string|array */ public function getPk() { return $this->pk; } /** * 判断一个字段名是否为主键字段. * * @param string $key 名称 * * @return bool */ protected function isPk(string $key): bool { $pk = $this->getPk(); if (is_string($pk) && $pk == $key) { return true; } elseif (is_array($pk) && in_array($key, $pk)) { return true; } return false; } /** * 获取模型对象的主键值 * * @return mixed */ public function getKey() { $pk = $this->getPk(); if (is_string($pk) && array_key_exists($pk, $this->data)) { return $this->data[$pk]; } } /** * 设置允许写入的字段. * * @param array $field 允许写入的字段 * * @return $this */ public function allowField(array $field) { $this->field = $field; return $this; } /** * 设置只读字段. * * @param array $field 只读字段 * * @return $this */ public function readonly(array $field) { $this->readonly = $field; return $this; } /** * 获取实际的字段名. * * @param string $name 字段名 * * @return string */ protected function getRealFieldName(string $name): string { if ($this->convertNameToCamel || !$this->strict) { return Str::snake($name); } return $name; } /** * 设置数据对象值 * * @param array|object $data 数据 * @param bool $set 是否调用修改器 * @param array $allow 允许的字段名 * * @return $this */ public function data(array | object $data, bool $set = false, array $allow = []) { if ($data instanceof Model) { $data = $data->getData(); } elseif (is_object($data)) { $data = get_object_vars($data); } // 清空数据 $this->data = []; // 废弃字段 foreach ($this->disuse as $key) { if (array_key_exists($key, $data)) { unset($data[$key]); } } if (!empty($allow)) { $result = []; foreach ($allow as $name) { if (isset($data[$name])) { $result[$name] = $data[$name]; } } $data = $result; } $this->appendData($data, $set); return $this; } /** * 批量追加数据对象值 * * @param array $data 数据 * @param bool $set 是否需要进行数据处理 * * @return $this */ public function appendData(array $data, bool $set = false) { if ($set) { $this->setAttrs($data); } else { $this->data = array_merge($this->data, $data); } return $this; } /** * 刷新对象原始数据(为当前数据). * * @return $this */ public function refreshOrigin() { $this->origin = $this->data; return $this; } /** * 获取对象原始数据 如果不存在指定字段返回null. * * @param string $name 字段名 留空获取全部 * * @return mixed */ public function getOrigin(?string $name = null) { if (is_null($name)) { return $this->origin; } $fieldName = $this->getRealFieldName($name); return array_key_exists($fieldName, $this->origin) ? $this->origin[$fieldName] : null; } /** * 获取当前对象数据 如果不存在指定字段返回false. * * @param string $name 字段名 留空获取全部 * * @throws InvalidArgumentException * * @return mixed */ public function getData(?string $name = null) { if (is_null($name)) { return $this->data; } $fieldName = $this->getRealFieldName($name); if (array_key_exists($fieldName, $this->data)) { return $this->data[$fieldName]; } if (array_key_exists($fieldName, $this->relation)) { return $this->relation[$fieldName]; } throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); } /** * 获取变化的数据 并排除只读数据. * * @return array */ public function getChangedData(): array { $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) { if ((empty($a) || empty($b)) && $a !== $b) { return 1; } if ($b instanceof Raw) { return 0; } return is_object($a) || $a != $b ? 1 : 0; }); // 只读字段不允许更新 foreach ($this->readonly as $field) { if (array_key_exists($field, $data)) { unset($data[$field]); } } return $data; } /** * 直接设置数据对象值 * * @param string $name 属性名 * @param mixed $value 值 * * @return void */ public function set(string $name, $value): void { $name = $this->getRealFieldName($name); $this->data[$name] = $value; unset($this->get[$name]); } /** * 通过修改器 批量设置数据对象值 * * @param array $data 数据 * * @return void */ public function setAttrs(array $data): void { // 进行数据处理 foreach ($data as $key => $value) { $this->setAttr($key, $value, $data); } } /** * 通过修改器 设置数据对象值 * * @param string $name 属性名 * @param mixed $value 属性值 * @param array $data 数据 * * @return void */ public function setAttr(string $name, $value, array $data = []): void { if ($this->mapping) { $name = array_search($name, $this->mapping) ?: $name; } $name = $this->getRealFieldName($name); // 检测修改器 $method = 'set' . Str::studly($name) . 'Attr'; if (method_exists($this, $method)) { $array = $this->data; $value = $this->$method($value, array_merge($this->data, $data)); if (is_null($value) && $array !== $this->data) { return; } } elseif (!in_array($name, $this->json) && isset($this->type[$name])) { // 类型转换 if ($this->enumStrict && is_subclass_of($this->type[$name], BackedEnum::class) && !($value instanceof BackedEnum)) { throw new InvalidArgumentException('data type error: ' . $name . ' => ' . $this->type[$name]); } $value = $this->writeTransform($value, $this->type[$name]); } elseif ($this->isRelationAttr($name)) { // 关联属性 $this->relation[$name] = $value; $this->with[$name] = true; } elseif ((array_key_exists($name, $this->origin) || empty($this->origin)) && $value instanceof Stringable) { // 对象类型 $value = $value->__toString(); } // 设置数据对象属性 $this->data[$name] = $value; unset($this->get[$name]); } /** * 数据写入 类型转换. * * @param mixed $value 值 * @param string|array $type 要转换的类型 * * @return mixed */ protected function writeTransform($value, string | array $type) { if (null === $value) { return; } if ($value instanceof Raw) { return $value; } if (is_array($type)) { [$type, $param] = $type; } elseif (str_contains($type, ':')) { [$type, $param] = explode(':', $type, 2); } $typeTransform = static function (string $type, $value, $model) { if (str_contains($type, '\\') && class_exists($type)) { if (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' => (string) $value, 'integer' => (int) $value, 'float' => empty($param) ? (float) $value : (float) number_format($value, (int) $param, '.', ''), 'boolean' => (bool) $value, 'timestamp' => !is_numeric($value) ? strtotime($value) : $value, 'datetime' => $this->formatDateTime('Y-m-d H:i:s.u', $value, true), 'object' => is_object($value) ? json_encode($value, JSON_FORCE_OBJECT) : $value, 'array' => json_encode((array) $value, !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE), 'json' => json_encode($value, !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE), 'serialize' => serialize($value), default => $typeTransform($type, $value, $this), }; } /** * 获取器 获取数据对象的值 * * @param string $name 名称 * * @throws InvalidArgumentException * * @return mixed */ public function getAttr(string $name) { try { $relation = false; if ($this->mapping) { $name = array_search($name, $this->mapping) ?: $name; } $value = $this->getData($name); } catch (InvalidArgumentException $e) { $relation = $this->isRelationAttr($name); $value = null; } return $this->getValue($name, $value, $relation); } /** * 获取经过获取器处理后的数据对象的值 * * @param string $name 字段名称 * @param mixed $value 字段值 * @param bool|string $relation 是否为关联属性或者关联名 * * @throws InvalidArgumentException * * @return mixed */ protected function getValue(string $name, $value, bool | string $relation = false) { // 检测属性获取器 $fieldName = $this->getRealFieldName($name); if (array_key_exists($fieldName, $this->get)) { return $this->get[$fieldName]; } $method = 'get' . Str::studly($name) . 'Attr'; if (isset($this->withAttr[$fieldName])) { if ($relation) { $value = $this->getRelationValue($relation); } if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) { $value = $this->getJsonValue($fieldName, $value); } else { $closure = $this->withAttr[$fieldName]; if ($closure instanceof \Closure) { $value = $closure($value, $this->data, $this); } } } elseif (method_exists($this, $method)) { if ($relation) { $value = $this->getRelationValue($relation); } $value = $this->$method($value, $this->data); } elseif (!in_array($fieldName, $this->json) && isset($this->type[$fieldName])) { // 类型转换 $value = $this->readTransform($value, $this->type[$fieldName]); } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) { $value = $this->getTimestampValue($value); } elseif ($relation) { $value = $this->getRelationValue($relation); // 保存关联对象值 $this->relation[$name] = $value; } $this->get[$fieldName] = $value; return $value; } /** * 获取JSON字段属性值 * * @param string $name 属性名 * @param mixed $value JSON数据 * * @return mixed */ protected function getJsonValue(string $name, $value) { if (is_null($value)) { return $value; } foreach ($this->withAttr[$name] as $key => $closure) { if ($this->jsonAssoc) { $value[$key] = $closure($value[$key] ?? '', $value); } else { $value->$key = $closure($value->$key ?? '', $value); } } return $value; } /** * 获取关联属性值 * * @param string $relation 关联名 * * @return mixed */ protected function getRelationValue(string $relation) { $modelRelation = $this->$relation(); return $modelRelation instanceof Relation ? $this->getRelationData($modelRelation) : null; } /** * 数据读取 类型转换. * * @param mixed $value 值 * @param string|array $type 要转换的类型 * * @return mixed */ protected function readTransform($value, string | array $type) { if (is_null($value)) { return; } if (is_array($type)) { [$type, $param] = $type; } elseif (str_contains($type, ':')) { [$type, $param] = explode(':', $type, 2); } $call = function ($value) { try { $value = unserialize($value); } catch (\Exception $e) { $value = null; } return $value; }; $typeTransform = static function (string $type, $value, $model) { if (str_contains($type, '\\') && class_exists($type)) { if (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->enumReadName) { $method = $model->enumReadName; $value = is_string($method) ? $value->$method() : $value->name; } } else { // 对象类型 $value = new $type($value); } } return $value; }; return match ($type) { 'string' => (string) $value, 'integer' => (int) $value, 'float' => empty($param) ? (float) $value : (float) number_format($value, (int) $param, '.', ''), 'boolean' => (bool) $value, 'timestamp' => !is_null($value) ? $this->formatDateTime(!empty($param) ? $param : $this->dateFormat, $value, true) : null, 'datetime' => !is_null($value) ? $this->formatDateTime(!empty($param) ? $param : $this->dateFormat, $value) : null, 'json' => json_decode($value, true), 'array' => empty($value) ? [] : json_decode($value, true), 'object' => empty($value) ? new \stdClass() : json_decode($value), 'serialize' => $call($value), default => $typeTransform($type, $value, $this), }; } /** * 设置数据字段获取器. * * @param string|array $name 字段名 * @param Closure $callback 闭包获取器 * * @return $this */ public function withFieldAttr(string | array $name, ?Closure $callback = null) { if (is_array($name)) { foreach ($name as $key => $val) { $this->withFieldAttr($key, $val); } } else { $name = $this->getRealFieldName($name); $this->append([$name], true); if (str_contains($name, '.')) { [$name, $key] = explode('.', $name); $this->withAttr[$name][$key] = $callback; } else { $this->withAttr[$name] = $callback; } } return $this; } /** * 设置枚举类型自动读取数据方式 * true 表示使用name值返回 * 字符串 表示使用枚举类的方法返回 * * @return $this */ public function withEnumRead(bool | string $method = true) { $this->enumReadName = $method; return $this; } }