hyperf 二十八 修改器 一

教程:Hyperf

一 修改器和访问器

根据教程,可设置相关函数,如set属性名Attribute()、get属性名Attribute(),设置和获取属性。这在thinkphp中也常见。

修改器:set属性名Attribute();访问器:get属性名Attribute()。

1.1 原理

模型的父类Hyperf\Database\Model\Model,定义__set()、_get()、__isset()、__unset()函数。

设置属性调用__set(),获取属性调用_get()。

__set()调用set属性名Attribute(),和格式化数据。先通过set属性名Attribute()获取值,再判断是否为日期格式化日期数据。若设置字段类型,会根据设定的字段类型匹配对应的类,返回对应类。会判断是否为json数据返回json格式字符换。若调用的对应字符串含有"->",则将该对应类对象格式化为json字符串返回。

1.2 测试

php 复制代码
#App\Controller\Test
public function testmodifier() {
        $result = Article::query()->find(2)->toArray();
        var_dump($result);
        $article = Article::firstOrCreate(
            ['title' => 'test4'],
            ['user_id' => 2]
        );
        $result = Article::query()->where(['title' => '&test4'])->first()->toArray();
        var_dump($result);
    }
php 复制代码
#App1\Model\Article
class Article extends Model implements CacheableInterface {
    use Cacheable;
    use SoftDeletes;
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'articles';
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['title', 'user_id']; //允许批量赋值
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = ['id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];

    public function setTitleAttribute($value) {
        $this->attributes['title'] = "&" . $value;
    }
    public function getTitleAttribute($value) {
        return "标题:" . $value;
    }
}

测试结果

php 复制代码
array(7) {
  ["id"]=>
  int(2)
  ["user_id"]=>
  int(1)
  ["title"]=>
  string(14) "标题: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)
}

array(7) {
  ["id"]=>
  int(10)
  ["user_id"]=>
  int(2)
  ["title"]=>
  string(15) "标题:&test4"
  ["created_at"]=>
  string(19) "2024-03-19 08:07:24"
  ["updated_at"]=>
  string(19) "2024-03-19 08:07:24"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(0)
}

数据保存使用Hyperf\Database\Model\Builder::firstOrCreate()。firstOrNew()仅在对象中增加数据,未保存进数据库,这是和firstOrCreate()的区别。

过程中创建Hyperf\Database\Model\Model类对象是__construct(),会调用Model::fill()。Model::fill()使用Model::isFillable()调用Model::fillable属性,结果为true,才能设置属性,否则报错。

因为在Article::setTitleAttribute()对传入的属性增加数据。根据测试代码,查询的使用也应该加上"&"。

也是因为使用Builder::firstOrCreate()和Article::setTitleAttribute()修改传入属性,设置查询数据时不会查询到相应数据,因为查询值有差异。

tp中也遇到过相似情况。解决方法,对查询条件中数据也进行数据的换装,保证修改方式和保存之前的数据方式一样。

1.3 源码

php 复制代码
#App1\Model\Article 

use Hyperf\DbConnection\Model\Model;

class Article extends Model implements CacheableInterface {
    use Cacheable;
    use SoftDeletes;
}


#Hyperf\DbConnection\Model\Model

use Hyperf\Database\Model\Model as BaseModel;

class Model extends BaseModel
{
    use HasContainer;
    use HasRepository;
}
php 复制代码
#Hyperf\Database\Model\Model

abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, 
CompressInterface {
    use Concerns\HasAttributes;
    use Concerns\HasEvents;
    use Concerns\HasGlobalScopes;
    use Concerns\HasRelationships;
    use Concerns\HasTimestamps;
    use Concerns\HidesAttributes;
    use Concerns\GuardsAttributes;

    /**
     * Dynamically retrieve attributes on the model.
     *
     * @param string $key
     */
    public function __get($key) {
        return $this->getAttribute($key);
    }

    /**
     * Dynamically set attributes on the model.
     *
     * @param string $key
     * @param mixed $value
     */
    public function __set($key, $value) {
        $this->setAttribute($key, $value);
    }

    /**
     * Determine if an attribute or relation exists on the model.
     *
     * @param string $key
     * @return bool
     */
    public function __isset($key) {
        return $this->offsetExists($key);
    }

    /**
     * Unset an attribute on the model.
     *
     * @param string $key
     */
    public function __unset($key) {
        $this->offsetUnset($key);
    }
   
}
php 复制代码
# Hyperf\Database\Model\Concerns\HasAttributes
 /**
     * Set a given attribute on the model.
     *
     * @param string $key
     * @param mixed $value
     */
    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;
    }
    /**
     * Set the value of an attribute using its mutator.
     *
     * @param string $key
     * @param mixed $value
     */
    protected function setMutatedAttributeValue($key, $value)
    {
        return $this->{'set' . Str::studly($key) . 'Attribute'}($value);
    }
     /**
     * Convert a DateTime to a storable string.
     *
     * @param mixed $value
     * @return null|string
     */
    public function fromDateTime($value)
    {
        return empty($value) ? $value : $this->asDateTime($value)->format(
            $this->getDateFormat()
        );
    }
     /**
     * Get the format for database stored dates.
     *
     * @return string
     */
    public function getDateFormat()
    {
        return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat();
    }
/**
     * Set the value of a class castable attribute.
     *
     * @param string $key
     * @param mixed $value
     */
    protected function setClassCastableAttribute($key, $value)
    {
        $caster = $this->resolveCasterClass($key);

        if (is_null($value)) {
            $this->attributes = array_merge($this->attributes, array_map(
                function () {
                },
                $this->normalizeCastClassResponse($key, $caster->set(
                    $this,
                    $key,
                    $this->{$key},
                    $this->attributes
                ))
            ));
        } else {
            $this->attributes = array_merge(
                $this->attributes,
                $this->normalizeCastClassResponse($key, $caster->set(
                    $this,
                    $key,
                    $value,
                    $this->attributes
                ))
            );
        }

        if ($caster instanceof CastsInboundAttributes || !is_object($value)) {
            unset($this->classCastCache[$key]);
        } else {
            $this->classCastCache[$key] = $value;
        }
    }
 /**
     * Cast the given attribute to JSON.
     *
     * @param string $key
     * @param mixed $value
     * @return string
     */
    protected function castAttributeAsJson($key, $value)
    {
        $value = $this->asJson($value);

        if ($value === false) {
            throw JsonEncodingException::forAttribute(
                $this,
                $key,
                json_last_error_msg()
            );
        }

        return $value;
    }
 /**
     * Set a given JSON attribute on the model.
     *
     * @param string $key
     * @param mixed $value
     * @return $this
     */
    public function fillJsonAttribute($key, $value)
    {
        [$key, $path] = explode('->', $key, 2);

        $this->attributes[$key] = $this->asJson($this->getArrayAttributeWithValue(
            $path,
            $key,
            $value
        ));

        return $this;
    }

二 日期转化及时间格式化

模型会将 created_atupdated_at 字段转换为 Carbon\Carbon 实例,它继承了 PHP 原生的 DateTime 类并提供了各种有用的方法。可以通过设置模型的 $dates 属性来添加其他日期属性。

2.1 原理

调用Model::_get()、Model::_set()时,会判断字段类型,为日期则转换为Carbon\Carbon类对象。可以设置日期格式。

date为日期类型字段,dateFormat为日期格式字符串,都在Hyperf\Database\Model\Concerns\HasAttributes中设置,也是由其转换数据类型。

HasAttributes::castAttribute()处理各种字段类型,HasAttributes::asDate()执行日期类型转换,HasAttributes::getDateFormat()获取日期格式。

日期类型默认包括created_at 、updated_at。日期``默认格式"Y-m-d H:i:s"。

2.2 测试

php 复制代码
 #App1\Model\Article  
 protected $dateFormat = 'Y-m-d H:i';
 public function setTitleAttribute($value) {
        $this->attributes['title'] = $value;
    }
    public function getTitleAttribute($value) {
        return $value;
    }
php 复制代码
#App\Controller\TestController
public function testmodifier() {
        $article = Article::firstOrCreate(
            ['title' => 'test4'],
            ['user_id' => 2]
        );
        var_dump($article->toArray());
    }

测试结果

php 复制代码
array(7) {
  ["id"]=>
  int(11)
  ["user_id"]=>
  int(2)
  ["title"]=>
  string(5) "test4"
  ["created_at"]=>
  string(16) "2024-03-22 09:04"
  ["updated_at"]=>
  string(16) "2024-03-22 09:04"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(0)
}

测试可见 数据库中时间格式还是h:i:s,仅获取的时候是h:i格式。

Model::CREATED_AT、Model::UPDATED_AT使用Carbon::now()获取时间,并没有使用$dateFormat属性。

2.3 源码

php 复制代码
#Hyperf\Database\Model\Model
public function __get($key) {
        return $this->getAttribute($key);
    }
public function __set($key, $value) {
        $this->setAttribute($key, $value);
    }
 /**
     * 新增时使用
     *
     * @param \Hyperf\Database\Model\Builder $query
     * @return bool
     */
    protected function performInsert(Builder $query) {
        if ($event = $this->fireModelEvent('creating')) {
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
                return false;
            }
        }

        // First we'll need to create a fresh query instance and touch the creation and
        // update timestamps on this model, which are maintained by us for developer
        // convenience. After, we will just continue saving these model instances.
        if ($this->usesTimestamps()) {
            $this->updateTimestamps();
        }

        // If the model has an incrementing key, we can use the "insertGetId" method on
        // the query builder, which will give us back the final inserted ID for this
        // table from the database. Not all tables have to be incrementing though.
        $attributes = $this->getAttributes();

        if ($this->getIncrementing()) {
            $this->insertAndSetId($query, $attributes);
        }

        // If the table isn't incrementing we'll simply insert these attributes as they
        // are. These attribute arrays must contain an "id" column previously placed
        // there by the developer as the manually determined key for these models.
        else {
            if (empty($attributes)) {
                return true;
            }

            $query->insert($attributes);
        }

        // We will go ahead and set the exists property to true, so that it is set when
        // the created event is fired, just in case the developer tries to update it
        // during the event. This will allow them to do so and run an update here.
        $this->exists = true;

        $this->wasRecentlyCreated = true;

        $this->fireModelEvent('created');

        return true;
    }
/**
     * 修改时使用
     *
     * @param \Hyperf\Database\Model\Builder $query
     * @return bool
     */
    protected function performUpdate(Builder $query) {
        // If the updating event returns false, we will cancel the update operation so
        // developers can hook Validation systems into their models and cancel this
        // operation if the model does not pass validation. Otherwise, we update.
        if ($event = $this->fireModelEvent('updating')) {
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
                return false;
            }
        }

        // First we need to create a fresh query instance and touch the creation and
        // update timestamp on the model which are maintained by us for developer
        // convenience. Then we will just continue saving the model instances.
        if ($this->usesTimestamps()) {
            $this->updateTimestamps();
        }

        // Once we have run the update operation, we will fire the "updated" event for
        // this model instance. This will allow developers to hook into these after
        // models are updated, giving them a chance to do any special processing.
        $dirty = $this->getDirty();

        if (count($dirty) > 0) {
            $this->setKeysForSaveQuery($query)->update($dirty);

            $this->syncChanges();

            $this->fireModelEvent('updated');
        }

        return true;
    }
 public function save(array $options = []): bool {
        $this->mergeAttributesFromClassCasts();

        $query = $this->newModelQuery();

        // If the "saving" event returns false we'll bail out of the save and return
        // false, indicating that the save failed. This provides a chance for any
        // listeners to cancel save operations if validations fail or whatever.
        if ($saving = $this->fireModelEvent('saving')) {
            if ($saving instanceof StoppableEventInterface && $saving->isPropagationStopped()) {
                return false;
            }
        }

        // If the model already exists in the database we can just update our record
        // that is already in this database using the current IDs in this "where"
        // clause to only update this model. Otherwise, we'll just insert them.
        if ($this->exists) {
            $saved = $this->isDirty() ? $this->performUpdate($query) : true;
        } else {
            // If the model is brand new, we'll insert it into our database and set the
            // ID attribute on the model to the value of the newly inserted row's ID
            // which is typically an auto-increment value managed by the database.
            $saved = $this->performInsert($query);

            if (!$this->getConnectionName() && $connection = $query->getConnection()) {
                $this->setConnection($connection->getName());
            }
        }

        // If the model is successfully saved, we need to do a few more things once
        // that is done. We will call the "saved" method here to run any actions
        // we need to happen after a model gets successfully saved right here.
        if ($saved) {
            $this->finishSave($options);
        }

        return $saved;
    }
php 复制代码
#Hyperf\Database\Model\Concerns\HasAttributes
 /**
     * Set a given attribute on the model.
     *
     * @param string $key
     * @param mixed $value
     */
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;
    }
 public function fromDateTime($value)
    {
        return empty($value) ? $value : $this->asDateTime($value)->format(
            $this->getDateFormat()
        );
    }
/**
     * Get an attribute from the model.
     *
     * @param string $key
     */
    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);
    }
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 asDate($value)
    {
        return $this->asDateTime($value)->startOfDay();
    }
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);
    }
public function getDateFormat()
    {
        return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat();
    }
php 复制代码
#Hyperf\Database\Grammar
public function getDateFormat()
    {
        return 'Y-m-d H:i:s';
    }
php 复制代码
#Hyperf\Database\Model\Concerns\HasTimestamps
protected function updateTimestamps()
    {
        $time = $this->freshTimestamp();

        if (! is_null(static::UPDATED_AT) && ! $this->isDirty(static::UPDATED_AT)) {
            $this->setUpdatedAt($time);
        }

        if (! $this->exists && ! is_null(static::CREATED_AT)
            && ! $this->isDirty(static::CREATED_AT)) {
            $this->setCreatedAt($time);
        }
    }
public function setCreatedAt($value)
    {
        $this->{static::CREATED_AT} = $value;

        return $this;
    }

public function setUpdatedAt($value)
    {
        $this->{static::UPDATED_AT} = $value;

        return $this;
    }
public function freshTimestamp()
    {
        return Carbon::now();
    }
php 复制代码
#Carbon\Traits\Creator
public function __construct($time = null, $tz = null)
    {
        if ($time instanceof DateTimeInterface) {
            $time = $this->constructTimezoneFromDateTime($time, $tz)->format('Y-m-d H:i:s.u');
        }

        if (is_numeric($time) && (!\is_string($time) || !preg_match('/^\d{1,14}$/', $time))) {
            $time = static::createFromTimestampUTC($time)->format('Y-m-d\TH:i:s.uP');
        }

        // If the class has a test now set and we are trying to create a now()
        // instance then override as required
        $isNow = empty($time) || $time === 'now';

        if (method_exists(static::class, 'hasTestNow') &&
            method_exists(static::class, 'getTestNow') &&
            static::hasTestNow() &&
            ($isNow || static::hasRelativeKeywords($time))
        ) {
            static::mockConstructorParameters($time, $tz);
        }

        // Work-around for PHP bug https://bugs.php.net/bug.php?id=67127
        if (!str_contains((string) .1, '.')) {
            $locale = setlocale(LC_NUMERIC, '0'); // @codeCoverageIgnore
            setlocale(LC_NUMERIC, 'C'); // @codeCoverageIgnore
        }

        try {
            parent::__construct($time ?: 'now', static::safeCreateDateTimeZone($tz) ?: null);
        } catch (Exception $exception) {
            throw new InvalidFormatException($exception->getMessage(), 0, $exception);
        }

        $this->constructedObjectId = spl_object_hash($this);

        if (isset($locale)) {
            setlocale(LC_NUMERIC, $locale); // @codeCoverageIgnore
        }

        self::setLastErrors(parent::getLastErrors());
    }
 public static function now($tz = null)
    {
        return new static(null, $tz);
    }
相关推荐
2401_857439693 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
Smile灬凉城6669 小时前
反序列化为啥可以利用加号绕过php正则匹配
开发语言·php
奥顺11 小时前
PHPUnit使用指南:编写高效的单元测试
大数据·mysql·开源·php
黑客Jack13 小时前
网络安全加密
安全·web安全·php
龙哥·三年风水16 小时前
workman服务端开发模式-应用开发-后端api推送修改二
分布式·gateway·php
计算机徐师兄17 小时前
基于TP5框架的家具购物小程序的设计与实现【附源码、文档】
小程序·php·家具购物小程序·家具购物微信小程序·家具购物
希雅不是希望18 小时前
Ubuntu命令行网络配置
网络·ubuntu·php
龙哥·三年风水20 小时前
workman服务端开发模式-应用开发-后端api推送修改一
分布式·gateway·php
开心工作室_kaic1 天前
springboot461学生成绩分析和弱项辅助系统设计(论文+源码)_kaic
开发语言·数据库·vue.js·php·apache
火³可²1 天前
PHP接入美团联盟推广
开发语言·php