教程:Hyperf
属性类型转换
Hyperf\Database\Model\Concerns\HasAttributes::casts被HasAttributes::setAttribute()、HasAttributes::getAttribute()调用,执行类型转换。
HasAttributes::casts为数组类型可设置基本类型或类的实例。默认设置主键id为int。
内置的基本强制转换类型:
- json:'array', 'json', 'object', 'collection'
- 日期:'date', 'datetime'
- 整数:'int'\integer
- 浮点数: real\float\double
- 数字:decimal
- 字符串:string
- 布尔:bool/boolean
- 自定义时间:custom_datetime
- 时间戳:timestamp
decimal 可设置小数位数,格式decimal:小数位数,通过number_format()实现。
一 自定义类型转换
通过继承接口(implements)Hyperf\Contract\CastsAttributes,定义好后使用其类名称将其附加到HasAttributes::casts。
1.1 值对象类型转换
将值转换成对象。通过get()获取时设置为对象、set()设置时将值设置到对象中。
执行save()之前需要设置相应的对象。官网例子中是先获取在设置,这样获取之后数据结构中自带类。
更改数据结构中类对象值后,因为缓存原因,其类的值不会通过HasAttributes::getAttributes()获得刷新,使用HasAttributes::syncAttributes()通过缓存和属性值合并可获取设置后的更新数据。
1.2 入站类型转换
设置set()实现入站类型转换。
1.3 类型参数转换
使用":"设置参数。
php
protected $casts = [
'secret' => Hash::class.':sha256',
];
1.4 测试
获取
php
#App\Controlle\TestController
public function testmodifier() {
$r = Article::query()->find(2);
$article = $r->article;
$istop = $r->is_top;
var_dump($istop);
$r->article->intro = "test1";
$info1 = $r->getAttributes();
$info2 = $r->syncAttributes()->getAttributes();
$r->intro = "test2";
$info3 = $r->getAttributes();
var_dump($article, $info1, $info2, $info3);
}
php
#App1\Model\Article
protected $casts = [
'id' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'is_top' => 'boolean',
'article' => ArticleCasts::class,
];
php
#App1\Attributes\Article
namespace App1\Attributes;
class Article {
public $title;
public $content;
public $intro;
public function __construct($title, $intro, $content) {
$this->title = $title;
$this->content = $content;
$this->intro = $intro;
}
}
php
#App1\Casts\ArticleCasts
namespace App1\Casts;
use App1\Attributes\Article;
use Hyperf\Contract\CastsAttributes;
class ArticleCasts implements CastsAttributes {
/**
* 将取出的数据进行转换
*/
public function get($model, $key, $value, $attributes): Article {
return new Article(
$attributes['title'],
$attributes['intro'],
$attributes['content']
);
}
/**
* 转换成将要进行存储的值
*/
public function set($model, $key, $value, $attributes) {
return [
'title' => $value->title,
'intro' => $value->intro,
'content' => $value->content,
];
}
}
测试结果
php
bool(false)
object(App1\Attributes\Article)#1213 (3) {
["title"]=>
string(5) "test2"
["content"]=>
NULL
["intro"]=>
string(5) "test1"
}
array(10) {
["id"]=>
int(2)
["user_id"]=>
int(1)
["title"]=>
string(5) "test2"
["created_at"]=>
string(19) "2024-01-13 10:06:04"
["updated_at"]=>
string(19) "2024-01-13 10:06:06"
["deleted_at"]=>
NULL
["pv_num"]=>
int(0)
["intro"]=>
NULL
["content"]=>
NULL
["is_top"]=>
int(0)
}
array(10) {
["id"]=>
int(2)
["user_id"]=>
int(1)
["title"]=>
string(5) "test2"
["created_at"]=>
string(19) "2024-01-13 10:06:04"
["updated_at"]=>
string(19) "2024-01-13 10:06:06"
["deleted_at"]=>
NULL
["pv_num"]=>
int(0)
["intro"]=>
string(5) "test1"
["content"]=>
NULL
["is_top"]=>
int(0)
}
array(10) {
["id"]=>
int(2)
["user_id"]=>
int(1)
["title"]=>
string(5) "test2"
["created_at"]=>
string(19) "2024-01-13 10:06:04"
["updated_at"]=>
string(19) "2024-01-13 10:06:06"
["deleted_at"]=>
NULL
["pv_num"]=>
int(0)
["intro"]=>
string(5) "test2"
["content"]=>
NULL
["is_top"]=>
int(0)
}
写入
php
#App\Controlle\TestController
public function testmodifier() {
$r = Article::query()->find(2);
$r->is_top = 1;
$r->article->intro = "test2";
$attr = $r->getAttributes();
var_dump($attr);
$r->save();
$attr = $r->getAttributes();
var_dump($attr);
}
测试结果
php
array(10) {
["id"]=>
int(2)
["user_id"]=>
int(1)
["title"]=>
string(5) "test2"
["created_at"]=>
string(19) "2024-01-13 10:06:04"
["updated_at"]=>
string(19) "2024-03-25 09:47:00"
["deleted_at"]=>
NULL
["pv_num"]=>
int(0)
["intro"]=>
string(5) "test1"
["content"]=>
NULL
["is_top"]=>
int(1)
}
array(10) {
["id"]=>
int(2)
["user_id"]=>
int(1)
["title"]=>
string(5) "test2"
["created_at"]=>
string(19) "2024-01-13 10:06:04"
["updated_at"]=>
string(16) "2024-03-25 09:48"
["deleted_at"]=>
NULL
["pv_num"]=>
int(0)
["intro"]=>
string(5) "test2"
["content"]=>
NULL
["is_top"]=>
int(1)
}
1.5 源码
转换类型
php
#Hyperf\Database\Model\Concerns\HasAttributes
public function getAttribute($key)
{
if (!$key) {
return;
}
// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
if (array_key_exists($key, $this->getAttributes())
|| $this->hasGetMutator($key)
|| $this->isClassCastable($key)) {
return $this->getAttributeValue($key);
}
// Here we will determine if the model base class itself contains this given key
// since we don't want to treat any of those methods as relationships because
// they are all intended as helper methods and none of these are relations.
if (method_exists(self::class, $key)) {
return;
}
return $this->getRelationValue($key);
}
protected function castAttribute($key, $value)
{
$castType = $this->getCastType($key);
if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
return $value;
}
switch ($castType) {
case 'int':
case 'integer':
return (int) $value;
case 'real':
case 'float':
case 'double':
return $this->fromFloat($value);
case 'decimal':
return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
case 'string':
return (string) $value;
case 'bool':
case 'boolean':
return (bool) $value;
case 'object':
return $this->fromJson($value, true);
case 'array':
case 'json':
return $this->fromJson($value);
case 'collection':
return new BaseCollection($this->fromJson($value));
case 'date':
return $this->asDate($value);
case 'datetime':
case 'custom_datetime':
return $this->asDateTime($value);
case 'timestamp':
return $this->asTimestamp($value);
}
if ($this->isClassCastable($key)) {
return $this->getClassCastableAttributeValue($key, $value);
}
return $value;
}
protected function asDecimal($value, $decimals)
{
return number_format((float) $value, (int) $decimals, '.', '');
}
protected function isDateCastable($key)
{
return $this->hasCast($key, ['date', 'datetime']);
}
protected function isJsonCastable($key)
{
return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
}
public function getAttributeValue($key)
{
return $this->transformModelValue($key, $this->getAttributeFromArray($key));
}
protected function transformModelValue($key, $value)
{
// If the attribute has a get mutator, we will call that then return what
// it returns as the value, which is useful for transforming values on
// retrieval from the model to a form that is more useful for usage.
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
// If the attribute exists within the cast array, we will convert it to
// an appropriate native PHP type dependent upon the associated value
// given with the key in the pair. Dayle made this comment line up.
if ($this->hasCast($key)) {
return $this->castAttribute($key, $value);
}
// If the attribute is listed as a date, we will convert it to a DateTime
// instance on retrieval, which makes it quite convenient to work with
// date fields without having to create a mutator for each property.
if ($value !== null
&& \in_array($key, $this->getDates(), false)) {
return $this->asDateTime($value);
}
return $value;
}
数据更新
php
#Hyperf\Database\Model\Concerns\HasAttributes
public function syncAttributes()
{
$this->mergeAttributesFromClassCasts();
return $this;
}
protected function mergeAttributesFromClassCasts()
{
foreach ($this->classCastCache as $key => $value) {
if ($value instanceof Synchronized && $value->isSynchronized()) {
continue;
}
$caster = $this->resolveCasterClass($key);
$this->attributes = array_merge(
$this->attributes,
$caster instanceof CastsInboundAttributes
? [$key => $value]
: $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes))
);
}
}
二 数组和json转换
数据库存json,数据库字段为text或json,数据获取时转变为数组。
获取时使用 Hyperf\Database\Model\Model::__get()调用Hyperf\Database\Model\Concerns\HasAttributes::getAttribute()。
设置时使用Model::__set()调用HasAttributes::setAttribute()。
判断是否可为json数据,则使用json_encode()转化为json数据保存。
根据源码,HasAttributes::casts数组中设置字段类型为array, json, object, collection中的类型,可为json字符串保存。
2.1 测试
php
#App\Controller\TestController
public function testmodifier() {
$article = Article::query()->find(2);
$options = $article->content;
var_dump($options);
if (empty($options)) {
$article->content = ['intro' => 'test', 'content' => 'test1'];
$article->save();
$options = $article->content;
}
var_dump($options);
}
测试结果
php
NULL
array(2) {
["intro"]=>
string(4) "test"
["content"]=>
string(5) "test1"
}
sql
select content from articles where id=2
{"intro":"test","content":"test1"}
2.2 源码
getAttribute()源码详见1.5源码。
php
#Hyperf\Database\Model\Concerns\HasAttributes
public function setAttribute($key, $value) {
// First we will check for the presence of a mutator for the set operation
// which simply lets the developers tweak the attribute as it is set on
// the model, such as "json_encoding" an listing of data for storage.
if ($this->hasSetMutator($key)) {
return $this->setMutatedAttributeValue($key, $value);
}
// If an attribute is listed as a "date", we'll convert it from a DateTime
// instance into a form proper for storage on the database tables using
// the connection grammar's date format. We will auto set the values.
if ($value && $this->isDateAttribute($key)) {
$value = $this->fromDateTime($value);
}
if ($this->isClassCastable($key)) {
$this->setClassCastableAttribute($key, $value);
return $this;
}
if ($this->isJsonCastable($key) && !is_null($value)) {
$value = $this->castAttributeAsJson($key, $value);
}
// If this attribute contains a JSON ->, we'll set the proper value in the
// attribute's underlying array. This takes care of properly nesting an
// attribute in the array's value in the case of deeply nested items.
if (Str::contains($key, '->')) {
return $this->fillJsonAttribute($key, $value);
}
$this->attributes[$key] = $value;
return $this;
}
protected function isJsonCastable($key) {
return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
}
public function hasCast($key, $types = null) {
if (array_key_exists($key, $this->getCasts())) {
return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
}
return false;
}
public function getCasts() {
if ($this->getIncrementing()) {
return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
}
return $this->casts;
}
protected function castAttributeAsJson($key, $value) {
$value = $this->asJson($value);
if ($value === false) {
throw JsonEncodingException::forAttribute(
$this,
$key,
json_last_error_msg()
);
}
return $value;
}
三 Date类型转换
在设置的时候,若不是Hyperf\Database\Model\Model::CREATED_AT,Model::UPDATED_AT,即字段不为created_at或updated_at,且设置参数如日期格式,则不会调用转换。
因为判断是否可转换的时候被过滤掉。像官网设置created_at不会被转换,可能和版本有关系。
再说获取。
直接获取对应属性,即运行Hyperf\Database\Model\Model::__get()。 转换类型会被判断为custom_datetime,转换为Carbon类。
但是设置的格式未被解析,还是需要手动设置格式。使用Hyperf\Database\Model\Concerns\HasAttributes::getAttributes()获取,直接获取HasAttributes::attributes属性。
这两种对与设置的格式都不会解析,返回都是根据HasAttributes::dateFormat,其默认值都是Y-m-d H:i:s。
使用HasAttributes::attributesToArray(),会调用HasAttributes::addCastAttributesToArray(),会判断是否为custom_datetime。
是custom_datetime类型,则会调用php系统类自带函数format(),通过解析参数,获得格式化数据。
其实和自己使用format(),运行原理一样,效果也一样。
3.1 测试
php
#App\Controller\TestController
public function testmodifier() {
$pr = PushRecode::query()->find(1);
$pr->push_time = date('Y-m-d H:i:s');
var_dump($pr->push_time);
$pr = $pr->syncAttributes();
var_dump($pr->push_time->format('Y-m-d H:00:00'));
$pr->created_at = date('Y-m-d H:i:s');
$info = $pr->syncAttributes()->getAttributes();
var_dump($info);
$info = $pr->syncAttributes()->attributesToArray();
var_dump($info);
}
测试结果
php
object(Carbon\Carbon)#1151 (19) {
["endOfTime":protected]=>
bool(false)
["startOfTime":protected]=>
bool(false)
["constructedObjectId":protected]=>
string(32) "00000000607fdc84000000003fa11939"
["localMonthsOverflow":protected]=>
NULL
["localYearsOverflow":protected]=>
NULL
["localStrictModeEnabled":protected]=>
NULL
["localHumanDiffOptions":protected]=>
NULL
["localToStringFormat":protected]=>
NULL
["localSerializer":protected]=>
NULL
["localMacros":protected]=>
NULL
["localGenericMacros":protected]=>
NULL
["localFormatFunction":protected]=>
NULL
["localTranslator":protected]=>
NULL
["dumpProperties":protected]=>
array(3) {
[0]=>
string(4) "date"
[1]=>
string(13) "timezone_type"
[2]=>
string(8) "timezone"
}
["dumpLocale":protected]=>
NULL
["dumpDateProperties":protected]=>
NULL
["date"]=>
string(26) "2024-04-11 09:30:03.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(3) "UTC"
}
string(19) "2024-04-11 09:00:00"
array(4) {
["id"]=>
int(1)
["is_push"]=>
int(1)
["push_time"]=>
string(19) "2024-04-11 09:30:03"
["created_at"]=>
string(19) "2024-04-11 09:30:03"
}
array(4) {
["id"]=>
int(1)
["is_push"]=>
int(1)
["push_time"]=>
string(10) "2024-04-11"
["created_at"]=>
string(10) "2024-04-11"
}
3.2 源码
Hyperf\Database\Model\Model::getAttribute()内容详见1.5源码。
Hyperf\Database\Model\Model::setAttribute()内容详见2.2源码。
php
#Hyperf\Database\Model\Concerns\HasAttributes
protected function isClassCastable($key) {
return array_key_exists($key, $this->getCasts())
&& class_exists($class = $this->parseCasterClass($this->getCasts()[$key]))
&& !in_array($class, static::$primitiveCastTypes);
}
protected function parseCasterClass($class) {
return strpos($class, ':') === false
? $class
: explode(':', $class, 2)[0];
}
protected static $primitiveCastTypes = [
'array',
'bool',
'boolean',
'collection',
'custom_datetime',
'date',
'datetime',
'decimal',
'double',
'float',
'int',
'integer',
'json',
'object',
'real',
'string',
'timestamp',
];
public function getCasts() {
if ($this->getIncrementing()) {
return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
}
return $this->casts;
}
public function getAttributes() {
return $this->attributes;
}
protected function isDateAttribute($key) {
return in_array($key, $this->getDates(), true)
|| $this->isDateCastable($key);
}
protected function isDateCastable($key) {
return $this->hasCast($key, ['date', 'datetime']);
}
public function getAttributeValue($key) {
return $this->transformModelValue($key, $this->getAttributeFromArray($key));
}
protected function transformModelValue($key, $value) {
// If the attribute has a get mutator, we will call that then return what
// it returns as the value, which is useful for transforming values on
// retrieval from the model to a form that is more useful for usage.
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
// If the attribute exists within the cast array, we will convert it to
// an appropriate native PHP type dependent upon the associated value
// given with the key in the pair. Dayle made this comment line up.
if ($this->hasCast($key)) {
return $this->castAttribute($key, $value);
}
// If the attribute is listed as a date, we will convert it to a DateTime
// instance on retrieval, which makes it quite convenient to work with
// date fields without having to create a mutator for each property.
if ($value !== null
&& \in_array($key, $this->getDates(), false)) {
return $this->asDateTime($value);
}
return $value;
}
protected function castAttribute($key, $value) {
$castType = $this->getCastType($key);
if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
return $value;
}
switch ($castType) {
case 'int':
case 'integer':
return (int) $value;
case 'real':
case 'float':
case 'double':
return $this->fromFloat($value);
case 'decimal':
return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
case 'string':
return (string) $value;
case 'bool':
case 'boolean':
return (bool) $value;
case 'object':
return $this->fromJson($value, true);
case 'array':
case 'json':
return $this->fromJson($value);
case 'collection':
return new BaseCollection($this->fromJson($value));
case 'date':
return $this->asDate($value);
case 'datetime':
case 'custom_datetime':
return $this->asDateTime($value);
case 'timestamp':
return $this->asTimestamp($value);
}
if ($this->isClassCastable($key)) {
return $this->getClassCastableAttributeValue($key, $value);
}
return $value;
}
protected function getCastType($key) {
if ($this->isCustomDateTimeCast($this->getCasts()[$key])) {
return 'custom_datetime';
}
if ($this->isDecimalCast($this->getCasts()[$key])) {
return 'decimal';
}
return trim(strtolower($this->getCasts()[$key]));
}
protected function asDateTime($value) {
// If this value is already a Carbon instance, we shall just return it as is.
// This prevents us having to re-instantiate a Carbon instance when we know
// it already is one, which wouldn't be fulfilled by the DateTime check.
if ($value instanceof Carbon || $value instanceof CarbonInterface) {
return Carbon::instance($value);
}
// If the value is already a DateTime instance, we will just skip the rest of
// these checks since they will be a waste of time, and hinder performance
// when checking the field. We will just return the DateTime right away.
if ($value instanceof DateTimeInterface) {
return Carbon::parse(
$value->format('Y-m-d H:i:s.u'),
$value->getTimezone()
);
}
// If this value is an integer, we will assume it is a UNIX timestamp's value
// and format a Carbon object from this timestamp. This allows flexibility
// when defining your date fields as they might be UNIX timestamps here.
if (is_numeric($value)) {
return Carbon::createFromTimestamp($value);
}
// If the value is in simply year, month, day format, we will instantiate the
// Carbon instances from that format. Again, this provides for simple date
// fields on the database, while still supporting Carbonized conversion.
if ($this->isStandardDateFormat($value)) {
return Carbon::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay());
}
$format = $this->getDateFormat();
// Finally, we will just assume this date is in the format used by default on
// the database connection and use that format to create the Carbon object
// that is returned back out to the developers after we convert it here.
if (Carbon::hasFormat($value, $format)) {
return Carbon::createFromFormat($format, $value);
}
return Carbon::parse($value);
}
protected function isCustomDateTimeCast($cast) {
return strncmp($cast, 'date:', 5) === 0
|| strncmp($cast, 'datetime:', 9) === 0;
}
根据源码,判断array_key_exists($key, this-\>getCasts())、class_exists(class = this-\>parseCasterClass(this->getCasts()[key\]))应为true,!in_array(class, static::$primitiveCastTypes)为false,所以isClassCastable()为false。
isDateAttribute()结果应该也为false。
之后执行getAttributeValue(),获取castType为custom_datetime,最终转换为Carbon\Carbon类。
php
#Hyperf\Database\Model\Concerns\HasAttributes
public function attributesToArray() {
// If an attribute is a date, we will cast it to a string after converting it
// to a DateTime / Carbon instance. This is so we will get some consistent
// formatting while accessing attributes vs. arraying / JSONing a model.
$attributes = $this->addDateAttributesToArray(
$attributes = $this->getArrayableAttributes()
);
$attributes = $this->addMutatedAttributesToArray(
$attributes,
$mutatedAttributes = $this->getMutatedAttributes()
);
// Next we will handle any casts that have been setup for this model and cast
// the values to their appropriate type. If the attribute has a mutator we
// will not perform the cast on those attributes to avoid any confusion.
$attributes = $this->addCastAttributesToArray(
$attributes,
$mutatedAttributes
);
// Here we will grab all of the appended, calculated attributes to this model
// as these attributes are not really in the attributes array, but are run
// when we need to array or JSON the model for convenience to the coder.
foreach ($this->getArrayableAppends() as $key) {
$attributes[$key] = $this->mutateAttributeForArray($key, null);
}
return $attributes;
}
protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes) {
foreach ($this->getCasts() as $key => $value) {
if (!array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) {
continue;
}
// Here we will cast the attribute. Then, if the cast is a date or datetime cast
// then we will serialize the date for the array. This will convert the dates
// to strings based on the date format specified for these Model models.
$attributes[$key] = $this->castAttribute(
$key,
$attributes[$key]
);
// If the attribute cast was a date or a datetime, we will serialize the date as
// a string. This allows the developers to customize how dates are serialized
// into an array without affecting how they are persisted into the storage.
if ($attributes[$key]
&& ($value === 'date' || $value === 'datetime')) {
$attributes[$key] = $this->serializeDate($attributes[$key]);
}
if ($attributes[$key] && $this->isCustomDateTimeCast($value)) {
$attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]);
}
if ($attributes[$key] instanceof Arrayable) {
$attributes[$key] = $attributes[$key]->toArray();
}
}
return $attributes;
}
官网打不开了......后续内容等官网好了再说吧......