7.x 新特性之自定义 Cast 及使用场景

未匹配的标注

在以往的版本中,Laravel 提供了一些默认的类型转换

<?php

protected $casts = [
  'is_admin' => 'boolean',
]

Laravel 7 引入了自定义 Cast,使得用户可以自定义 Cast。那么,如何在项目中使用自定义 Cast 呢?下面我将列举三个场景帮助大家理解。

示例 1 - 替代模型事件

对于一些数据库字段来说,我们希望能够对其进行加密后保存。

在以往的版本中,一种较好的解决方式就是对保存和读取事件进行监听,实现属性自动加密和解密。

app/Traits/Encryptable.php

<?php

namespace App\Traits;

use Illuminate\Support\Facades\Crypt;

trait Encryptable
{
    public static function bootEncryptable()
    {
        static::saving(function($model){
            foreach ($model->encryptable as $attribute){
                $model->$attribute = Crypt::encrypt($model->$attribute);
            }
        });

        static::retrieved(function($model){
            foreach ($model->encryptable as $attribute){
                $model->$attribute = Crypt::decrypt($model->$attribute);
            }
        });
    }
}

使用

App\User.php

<?php

use App\Traits\Encryptable;

class User
{
       use Encryptable;
    protected $encryptable = ['name'];
}

该方式可以用自定义 Cast 实现。首先创建自定义 Cast 类,命名建议以 Cast 结尾,该类需要实现 CastsAttributes 接口

app/Casts/EncryptCast.php

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Facades\Crypt;

class EncryptCast implements CastsAttributes
{

    /**
     * @inheritDoc
     */
    public function get($model, string $key, $value, array $attributes)
    {        
        return Crypt::decrypt($value);
    }

    /**
     * @inheritDoc
     */
    public function set($model, string $key, $value, array $attributes)
    {
        return Crypt::encrypt($value);
    }
}

使用

App\User.php

<?php

class User
{
    protected $casts = [
        'name' => EncryptCast::class,
    ];
}

通过自定义 Cast,可以更直观的管理特性属性的转换。

示例 2 - 封装属性

对于一些基本的数据类型,比如 emailphone 等等,可以将其封装成类,这样可以做有很多好处

  • 验证类型的正确性
  • 更清晰的显示信息
  • 绑定关联的行为

我们以 Email 为例,可以创建 Email 类来表示

app/Email.php

<?php

namespace App;

use Illuminate\Support\Str;
use InvalidArgumentException;

class Email
{
    public $address;

    /**
     * Email constructor.
     *
     * @param $address
     */
    public function __construct($address)
    {
        if (! filter_var($address, FILTER_VALIDATE_EMAIL)){
            throw new InvalidArgumentException("非法的邮件地址");
        }
        $this->address = $address;
    }

    /**
     * @return string
     */
    public function domain()
    {
        return Str::of($this->address)->after('@')->__toString();
    }

    public function __toString()
    {
        return $this->address;
    }
}

将 email 封装成类,可以对 Email 类型进行验证,同时也可以提供一些相关的行为,比如获取邮件的域名。测试

<?php

$email = new Email('foo@bar.com');
$email->domain();  // bar.com

对于基本类型的封装,感兴趣的可以查看这篇文章 PHP 技巧 - 封装基本的数据类型

Laravel 的自定义 Cast 可以方便的集成用户的自定义类型。

App\Casts\EmailCast.php

<?php

namespace App\Casts;

use App\Email;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class EmailCast implements CastsAttributes
{

    /**
     * @inheritDoc
     */
    public function get($model, string $key, $value, array $attributes)
    {
        return new Email($value);
    }

    /**
     * @inheritDoc
     */
    public function set($model, string $key, $value, array $attributes)
    {
        return $value->address;
    }
}

使用

<?php

use App\Casts\EmailCast;

class User
{        
       protected $casts = [
        'email' => EmailCast::class,
    ];
}

测试

<?php

$user = User::first();
$user->email = new Email('foo@bar.com');  // 传入类型实例可以确保数据的正确性
$user->save();

$user->email->domain(); // bar.com
$user->email->address; // foo@bar.com
(string) $user->email; // foo@bar.com

示例 3 - 同时管理多个属性

Vocation (假期) 类有三个属性:名称、开始时间、结束时间

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Vocation extends Model
{
    protected $fillable = [
        'name', 'start', 'end'
    ]
}

开始时间和结束时间构成了一个时间表,可以使用自定义 Cast 同时管理这两者。

创建基本类型

App\Schedule.php

<?php

namespace App;

use Carbon\Carbon;

class Schedule
{
    public $start;
    public $end;

    /**
     * Schedule constructor.
     *
     * @param $start
     * @param $end
     */
    public function __construct($start, $end)
    {
        $this->start = $start;
        $this->end = $end;
    }

    /**
     * 假期是否快结束
     *
     * @return bool
     */
    public function isNearEnd() : bool
    {
        return Carbon::parse($this->end)->lte(Carbon::tomorrow());
    }
}

创建对应的 Cast

App\Casts\ScheduleCast.php

<?php

namespace App\Casts;

use App\Schedule;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class ScheduleCast implements CastsAttributes
{

    /**
     * @inheritDoc
     */
    public function get($model, string $key, $value, array $attributes)
    {
        return new Schedule(
            $attributes['start'],
            $attributes['end']
        );
    }

    /**
     * @inheritDoc
     */
    public function set($model, string $key, $value, array $attributes)
    {
        return [
            'start' => $value->start,
            'end' => $value->end,
        ];
    }
}

使用

<?php

namespace App;

use App\Casts\ScheduleCast;
use Illuminate\Database\Eloquent\Model;

class Vocation extends Model
{
    protected $casts = [
        'schedule' => ScheduleCast::class
    ];
}

运行

<?php

$vocation = Vocation::create([
  'name' => '测试',
  'start' => now(),
  'end' => now()->addDay()
]);

$vocation->schedule->isNearEnd(); // true
$vocation->schedule->end = now()->addWeek();
$vocation->schedule->isNearEnd(); // false
$vocation->save();

总结

自定义 Cast 清晰地定义了类型转换的职责,减少了「胖模型」的复杂度,相信该特性会被广泛使用。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
发起讨论 查看所有版本


暂无话题~