657 lines
18 KiB
PHP
657 lines
18 KiB
PHP
<?php
|
|
// +----------------------------------------------------------------------
|
|
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
|
// +----------------------------------------------------------------------
|
|
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
|
|
// +----------------------------------------------------------------------
|
|
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
|
// +----------------------------------------------------------------------
|
|
// | Author: liu21st <liu21st@gmail.com>
|
|
// +----------------------------------------------------------------------
|
|
|
|
namespace think\model\concern;
|
|
|
|
use InvalidArgumentException;
|
|
use think\db\Expression;
|
|
use think\Exception;
|
|
use think\Loader;
|
|
use think\model\Relation;
|
|
|
|
trait Attribute
|
|
{
|
|
/**
|
|
* 数据表主键 复合主键使用数组定义
|
|
* @var string|array
|
|
*/
|
|
protected $pk = 'id';
|
|
|
|
/**
|
|
* 数据表字段信息 留空则自动获取
|
|
* @var array
|
|
*/
|
|
protected $field = [];
|
|
|
|
/**
|
|
* JSON数据表字段
|
|
* @var array
|
|
*/
|
|
protected $json = [];
|
|
|
|
/**
|
|
* JSON数据取出是否需要转换为数组
|
|
* @var bool
|
|
*/
|
|
protected $jsonAssoc = false;
|
|
|
|
/**
|
|
* JSON数据表字段类型
|
|
* @var array
|
|
*/
|
|
protected $jsonType = [];
|
|
|
|
/**
|
|
* 数据表废弃字段
|
|
* @var array
|
|
*/
|
|
protected $disuse = [];
|
|
|
|
/**
|
|
* 数据表只读字段
|
|
* @var array
|
|
*/
|
|
protected $readonly = [];
|
|
|
|
/**
|
|
* 数据表字段类型
|
|
* @var array
|
|
*/
|
|
protected $type = [];
|
|
|
|
/**
|
|
* 当前模型数据
|
|
* @var array
|
|
*/
|
|
private $data = [];
|
|
|
|
/**
|
|
* 修改器执行记录
|
|
* @var array
|
|
*/
|
|
private $set = [];
|
|
|
|
/**
|
|
* 原始数据
|
|
* @var array
|
|
*/
|
|
private $origin = [];
|
|
|
|
/**
|
|
* 动态获取器
|
|
* @var array
|
|
*/
|
|
private $withAttr = [];
|
|
|
|
/**
|
|
* 获取模型对象的主键
|
|
* @access public
|
|
* @return string|array
|
|
*/
|
|
public function getPk()
|
|
{
|
|
return $this->pk;
|
|
}
|
|
|
|
/**
|
|
* 判断一个字段名是否为主键字段
|
|
* @access public
|
|
* @param string $key 名称
|
|
* @return bool
|
|
*/
|
|
protected function isPk($key)
|
|
{
|
|
$pk = $this->getPk();
|
|
if (is_string($pk) && $pk == $key) {
|
|
return true;
|
|
} elseif (is_array($pk) && in_array($key, $pk)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 获取模型对象的主键值
|
|
* @access public
|
|
* @return integer
|
|
*/
|
|
public function getKey()
|
|
{
|
|
$pk = $this->getPk();
|
|
if (is_string($pk) && array_key_exists($pk, $this->data)) {
|
|
return $this->data[$pk];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* 设置允许写入的字段
|
|
* @access public
|
|
* @param array|string|true $field 允许写入的字段 如果为true只允许写入数据表字段
|
|
* @return $this
|
|
*/
|
|
public function allowField($field)
|
|
{
|
|
if (is_string($field)) {
|
|
$field = explode(',', $field);
|
|
}
|
|
|
|
$this->field = $field;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 设置只读字段
|
|
* @access public
|
|
* @param array|string $field 只读字段
|
|
* @return $this
|
|
*/
|
|
public function readonly($field)
|
|
{
|
|
if (is_string($field)) {
|
|
$field = explode(',', $field);
|
|
}
|
|
|
|
$this->readonly = $field;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 设置数据对象值
|
|
* @access public
|
|
* @param mixed $data 数据或者属性名
|
|
* @param mixed $value 值
|
|
* @return $this
|
|
*/
|
|
public function data($data, $value = null)
|
|
{
|
|
if (is_string($data)) {
|
|
$this->data[$data] = $value;
|
|
return $this;
|
|
}
|
|
|
|
// 清空数据
|
|
$this->data = [];
|
|
|
|
if (is_object($data)) {
|
|
$data = get_object_vars($data);
|
|
}
|
|
|
|
if ($this->disuse) {
|
|
// 废弃字段
|
|
foreach ((array) $this->disuse as $key) {
|
|
if (array_key_exists($key, $data)) {
|
|
unset($data[$key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (true === $value) {
|
|
// 数据对象赋值
|
|
foreach ($data as $key => $value) {
|
|
$this->setAttr($key, $value, $data);
|
|
}
|
|
} elseif (is_array($value)) {
|
|
foreach ($value as $name) {
|
|
if (isset($data[$name])) {
|
|
$this->data[$name] = $data[$name];
|
|
}
|
|
}
|
|
} else {
|
|
$this->data = $data;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 批量设置数据对象值
|
|
* @access public
|
|
* @param mixed $data 数据
|
|
* @param bool $set 是否需要进行数据处理
|
|
* @return $this
|
|
*/
|
|
public function appendData($data, $set = false)
|
|
{
|
|
if ($set) {
|
|
// 进行数据处理
|
|
foreach ($data as $key => $value) {
|
|
$this->setAttr($key, $value, $data);
|
|
}
|
|
} else {
|
|
if (is_object($data)) {
|
|
$data = get_object_vars($data);
|
|
}
|
|
|
|
$this->data = array_merge($this->data, $data);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 获取对象原始数据 如果不存在指定字段返回null
|
|
* @access public
|
|
* @param string $name 字段名 留空获取全部
|
|
* @return mixed
|
|
*/
|
|
public function getOrigin($name = null)
|
|
{
|
|
if (is_null($name)) {
|
|
return $this->origin;
|
|
}
|
|
return array_key_exists($name, $this->origin) ? $this->origin[$name] : null;
|
|
}
|
|
|
|
/**
|
|
* 获取对象原始数据 如果不存在指定字段返回false
|
|
* @access public
|
|
* @param string $name 字段名 留空获取全部
|
|
* @return mixed
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function getData($name = null)
|
|
{
|
|
if (is_null($name)) {
|
|
return $this->data;
|
|
} elseif (array_key_exists($name, $this->data)) {
|
|
return $this->data[$name];
|
|
} elseif (array_key_exists($name, $this->relation)) {
|
|
return $this->relation[$name];
|
|
}
|
|
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
|
|
}
|
|
|
|
/**
|
|
* 获取变化的数据 并排除只读数据
|
|
* @access public
|
|
* @return array
|
|
*/
|
|
public function getChangedData()
|
|
{
|
|
if ($this->force) {
|
|
$data = $this->data;
|
|
} else {
|
|
$data = array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
|
|
if ((empty($a) || empty($b)) && $a !== $b) {
|
|
return 1;
|
|
}
|
|
|
|
return is_object($a) || $a != $b ? 1 : 0;
|
|
});
|
|
}
|
|
|
|
if (!empty($this->readonly)) {
|
|
// 只读字段不允许更新
|
|
foreach ($this->readonly as $key => $field) {
|
|
if (isset($data[$field])) {
|
|
unset($data[$field]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* 修改器 设置数据对象值
|
|
* @access public
|
|
* @param string $name 属性名
|
|
* @param mixed $value 属性值
|
|
* @param array $data 数据
|
|
* @return void
|
|
*/
|
|
public function setAttr($name, $value, $data = [])
|
|
{
|
|
if (isset($this->set[$name])) {
|
|
return;
|
|
}
|
|
|
|
if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
|
|
// 自动写入的时间戳字段
|
|
$value = $this->autoWriteTimestamp($name);
|
|
} else {
|
|
// 检测修改器
|
|
$method = 'set' . Loader::parseName($name, 1) . 'Attr';
|
|
|
|
if (method_exists($this, $method)) {
|
|
$origin = $this->data;
|
|
$value = $this->$method($value, array_merge($this->data, $data));
|
|
|
|
$this->set[$name] = true;
|
|
if (is_null($value) && $origin !== $this->data) {
|
|
return;
|
|
}
|
|
} elseif (isset($this->type[$name])) {
|
|
// 类型转换
|
|
$value = $this->writeTransform($value, $this->type[$name]);
|
|
}
|
|
}
|
|
|
|
// 设置数据对象属性
|
|
$this->data[$name] = $value;
|
|
}
|
|
|
|
/**
|
|
* 是否需要自动写入时间字段
|
|
* @access public
|
|
* @param bool $auto
|
|
* @return $this
|
|
*/
|
|
public function isAutoWriteTimestamp($auto)
|
|
{
|
|
$this->autoWriteTimestamp = $auto;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 自动写入时间戳
|
|
* @access protected
|
|
* @param string $name 时间戳字段
|
|
* @return mixed
|
|
*/
|
|
protected function autoWriteTimestamp($name)
|
|
{
|
|
if (isset($this->type[$name])) {
|
|
$type = $this->type[$name];
|
|
|
|
if (strpos($type, ':')) {
|
|
list($type, $param) = explode(':', $type, 2);
|
|
}
|
|
|
|
switch ($type) {
|
|
case 'datetime':
|
|
case 'date':
|
|
$value = $this->formatDateTime('Y-m-d H:i:s.u');
|
|
break;
|
|
case 'timestamp':
|
|
case 'integer':
|
|
default:
|
|
$value = time();
|
|
break;
|
|
}
|
|
} elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
|
|
'datetime',
|
|
'date',
|
|
'timestamp',
|
|
])) {
|
|
$value = $this->formatDateTime('Y-m-d H:i:s.u');
|
|
} else {
|
|
$value = time();
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* 数据写入 类型转换
|
|
* @access protected
|
|
* @param mixed $value 值
|
|
* @param string|array $type 要转换的类型
|
|
* @return mixed
|
|
*/
|
|
protected function writeTransform($value, $type)
|
|
{
|
|
if (is_null($value)) {
|
|
return;
|
|
}
|
|
|
|
if ($value instanceof Expression) {
|
|
return $value;
|
|
}
|
|
|
|
if (is_array($type)) {
|
|
list($type, $param) = $type;
|
|
} elseif (strpos($type, ':')) {
|
|
list($type, $param) = explode(':', $type, 2);
|
|
}
|
|
|
|
switch ($type) {
|
|
case 'integer':
|
|
$value = (int) $value;
|
|
break;
|
|
case 'float':
|
|
if (empty($param)) {
|
|
$value = (float) $value;
|
|
} else {
|
|
$value = (float) number_format($value, $param, '.', '');
|
|
}
|
|
break;
|
|
case 'boolean':
|
|
$value = (bool) $value;
|
|
break;
|
|
case 'timestamp':
|
|
if (!is_numeric($value)) {
|
|
$value = strtotime($value);
|
|
}
|
|
break;
|
|
case 'datetime':
|
|
$value = is_numeric($value) ? $value : strtotime($value);
|
|
$value = $this->formatDateTime('Y-m-d H:i:s.u', $value);
|
|
break;
|
|
case 'object':
|
|
if (is_object($value)) {
|
|
$value = json_encode($value, JSON_FORCE_OBJECT);
|
|
}
|
|
break;
|
|
case 'array':
|
|
$value = (array) $value;
|
|
case 'json':
|
|
$option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE;
|
|
$value = json_encode($value, $option);
|
|
break;
|
|
case 'serialize':
|
|
$value = serialize($value);
|
|
break;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* 获取器 获取数据对象的值
|
|
* @access public
|
|
* @param string $name 名称
|
|
* @param array $item 数据
|
|
* @return mixed
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function getAttr($name, &$item = null)
|
|
{
|
|
try {
|
|
$notFound = false;
|
|
$value = $this->getData($name);
|
|
} catch (InvalidArgumentException $e) {
|
|
$notFound = true;
|
|
$value = null;
|
|
}
|
|
|
|
// 检测属性获取器
|
|
$fieldName = Loader::parseName($name);
|
|
$method = 'get' . Loader::parseName($name, 1) . 'Attr';
|
|
|
|
if (isset($this->withAttr[$fieldName])) {
|
|
if ($notFound && $relation = $this->isRelationAttr($name)) {
|
|
$modelRelation = $this->$relation();
|
|
$value = $this->getRelationData($modelRelation);
|
|
}
|
|
|
|
$closure = $this->withAttr[$fieldName];
|
|
$value = $closure($value, $this->data);
|
|
} elseif (method_exists($this, $method)) {
|
|
if ($notFound && $relation = $this->isRelationAttr($name)) {
|
|
$modelRelation = $this->$relation();
|
|
$value = $this->getRelationData($modelRelation);
|
|
}
|
|
|
|
$value = $this->$method($value, $this->data);
|
|
} elseif (isset($this->type[$name])) {
|
|
// 类型转换
|
|
$value = $this->readTransform($value, $this->type[$name]);
|
|
} elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
|
|
if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
|
|
'datetime',
|
|
'date',
|
|
'timestamp',
|
|
])) {
|
|
$value = $this->formatDateTime($this->dateFormat, $value);
|
|
} else {
|
|
$value = $this->formatDateTime($this->dateFormat, $value, true);
|
|
}
|
|
} elseif ($notFound) {
|
|
$value = $this->getRelationAttribute($name, $item);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* 获取关联属性值
|
|
* @access protected
|
|
* @param string $name 属性名
|
|
* @param array $item 数据
|
|
* @return mixed
|
|
*/
|
|
protected function getRelationAttribute($name, &$item)
|
|
{
|
|
$relation = $this->isRelationAttr($name);
|
|
|
|
if ($relation) {
|
|
$modelRelation = $this->$relation();
|
|
if ($modelRelation instanceof Relation) {
|
|
$value = $this->getRelationData($modelRelation);
|
|
|
|
if ($item && method_exists($modelRelation, 'getBindAttr') && $bindAttr = $modelRelation->getBindAttr()) {
|
|
|
|
foreach ($bindAttr as $key => $attr) {
|
|
$key = is_numeric($key) ? $attr : $key;
|
|
|
|
if (isset($item[$key])) {
|
|
throw new Exception('bind attr has exists:' . $key);
|
|
} else {
|
|
$item[$key] = $value ? $value->getAttr($attr) : null;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// 保存关联对象值
|
|
$this->relation[$name] = $value;
|
|
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
|
|
}
|
|
|
|
/**
|
|
* 数据读取 类型转换
|
|
* @access protected
|
|
* @param mixed $value 值
|
|
* @param string|array $type 要转换的类型
|
|
* @return mixed
|
|
*/
|
|
protected function readTransform($value, $type)
|
|
{
|
|
if (is_null($value)) {
|
|
return;
|
|
}
|
|
|
|
if (is_array($type)) {
|
|
list($type, $param) = $type;
|
|
} elseif (strpos($type, ':')) {
|
|
list($type, $param) = explode(':', $type, 2);
|
|
}
|
|
|
|
switch ($type) {
|
|
case 'integer':
|
|
$value = (int) $value;
|
|
break;
|
|
case 'float':
|
|
if (empty($param)) {
|
|
$value = (float) $value;
|
|
} else {
|
|
$value = (float) number_format($value, $param, '.', '');
|
|
}
|
|
break;
|
|
case 'boolean':
|
|
$value = (bool) $value;
|
|
break;
|
|
case 'timestamp':
|
|
if (!is_null($value)) {
|
|
$format = !empty($param) ? $param : $this->dateFormat;
|
|
$value = $this->formatDateTime($format, $value, true);
|
|
}
|
|
break;
|
|
case 'datetime':
|
|
if (!is_null($value)) {
|
|
$format = !empty($param) ? $param : $this->dateFormat;
|
|
$value = $this->formatDateTime($format, $value);
|
|
}
|
|
break;
|
|
case 'json':
|
|
$value = json_decode($value, true);
|
|
break;
|
|
case 'array':
|
|
$value = empty($value) ? [] : json_decode($value, true);
|
|
break;
|
|
case 'object':
|
|
$value = empty($value) ? new \stdClass() : json_decode($value);
|
|
break;
|
|
case 'serialize':
|
|
try {
|
|
$value = unserialize($value);
|
|
} catch (\Exception $e) {
|
|
$value = null;
|
|
}
|
|
break;
|
|
default:
|
|
if (false !== strpos($type, '\\')) {
|
|
// 对象类型
|
|
$value = new $type($value);
|
|
}
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* 设置数据字段获取器
|
|
* @access public
|
|
* @param string|array $name 字段名
|
|
* @param callable $callback 闭包获取器
|
|
* @return $this
|
|
*/
|
|
public function withAttribute($name, $callback = null)
|
|
{
|
|
if (is_array($name)) {
|
|
foreach ($name as $key => $val) {
|
|
$key = Loader::parseName($key);
|
|
|
|
$this->withAttr[$key] = $val;
|
|
}
|
|
} else {
|
|
$name = Loader::parseName($name);
|
|
|
|
$this->withAttr[$name] = $callback;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
}
|