使用实体类

CodeIgniter 支持实体类作为一等公民,同时让它们的使用完全可选。它们通常用作存储库模式的一部分,但如果更适合你的需求,也可以直接与 Model 一起使用。

实体用法

从本质上讲,实体类只是一个表示单个数据库行的类。它具有类属性来表示数据库列,并提供任何其他方法来实现该行的业务逻辑。

注意

为了便于理解,此处的解释基于使用数据库的情况。但是,实体也可以用于不来自数据库的数据。

核心功能是它不知道如何持久化自身。这是模型或存储库类的责任。这样,如果你需要保存对象的方式有任何变化,你不需要改变该对象在整个应用程序中的使用方式。

这使得在快速原型制作阶段使用 JSON 或 XML 文件来存储对象成为可能,然后在你证明概念可行时轻松切换到数据库。

让我们浏览一个非常简单的用户实体,以及我们如何使用它来帮助澄清问题。

假设你有一个名为 users 的数据库表,其具有以下模式

id          - integer
username    - string
email       - string
password    - string
created_at  - datetime

重要

attributes 是内部使用的保留字。如果你将它用作列名,实体将无法正常工作。

创建实体类

现在创建一个新的实体类。由于没有存储这些类的默认位置,并且它不适合现有的目录结构,因此在 **app/Entities** 中创建一个新目录。在 **app/Entities/User.php** 中创建实体本身。

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    // ...
}

最简单地说,这就是你需要做的所有事情,尽管我们很快会让它更有用。

创建模型

首先在 **app/Models/UserModel.php** 中创建模型,以便我们可以与它交互

<?php

namespace App\Models;

use CodeIgniter\Model;

class UserModel extends Model
{
    protected $table         = 'users';
    protected $allowedFields = [
        'username', 'email', 'password',
    ];
    protected $returnType    = \App\Entities\User::class;
    protected $useTimestamps = true;
}

该模型在数据库中使用 users 表进行所有活动。我们已经设置了 $allowedFields 属性,以包括我们希望外部类更改的所有字段。 idcreated_atupdated_at 字段由类或数据库自动处理,因此我们不想更改它们。最后,我们已将我们的实体类设置为 $returnType。这确保了模型上的所有从数据库返回行的函数都将返回我们用户实体类的实例,而不是像往常一样的对象或数组。

使用实体类

现在所有部分都已到位,你可以像使用任何其他类一样使用实体类

<?php

$user = $userModel->find($id);

// Display
echo $user->username;
echo $user->email;

// Updating
unset($user->username);

if (! isset($user->username)) {
    $user->username = 'something new';
}

$userModel->save($user);

// Create
$user           = new \App\Entities\User();
$user->username = 'foo';
$user->email    = '[email protected]';
$userModel->save($user);

你可能已经注意到 User 类没有为列设置任何属性,但你仍然可以像访问公共属性一样访问它们。基类 CodeIgniter\Entity\Entity 会为你处理好这一点,并提供使用 isset()unset() 检查属性以及跟踪自对象创建或从数据库中提取以来哪些列已更改的能力。

注意

Entity 类内部将数据存储在类属性 $attributes 中。

当将 User 传递给模型的 save() 方法时,它会自动处理读取属性并保存对模型的 $allowedFields 属性中列出的列的任何更改。它还知道是创建新行还是更新现有行。

注意

当我们调用 insert() 时,Entity 中的所有值都会传递给该方法,但当我们调用 update() 时,则只传递已更改的值。

快速填充属性

Entity 类还提供了一个方法 fill(),它允许你将一个键/值对数组推入类并填充类属性。数组中的任何属性都将在 Entity 上设置。但是,通过模型保存时,只有 $allowedFields 中的字段才会实际保存到数据库,因此你可以将其他数据存储在你的实体上,而不用担心杂散字段被错误地保存。

<?php

$data = $this->request->getPost();

$user = new \App\Entities\User();
$user->fill($data);
$userModel->save($user);

你还可以将数据传递给构造函数,并且数据将在实例化期间通过 fill() 方法传递。

<?php

$data = $this->request->getPost();

$user = new \App\Entities\User($data);
$userModel->save($user);

批量访问属性

Entity 类有两种方法可以将所有可用属性提取到一个数组中:toArray()toRawArray()。使用原始版本将绕过魔术“getter”方法和强制转换。这两种方法都可以采用一个布尔值作为第一个参数,以指定返回的值是否应该按已更改的值进行筛选,并采用一个布尔值作为最后一个参数,以使该方法具有递归性(针对嵌套的 Entity)。

处理业务逻辑

虽然上面的示例很方便,但它们并不能帮助执行任何业务逻辑。基础 Entity 类实现了一些智能的 __get()__set() 方法,这些方法将检查特殊方法并使用它们而不是直接使用属性,从而允许你执行所需的任何业务逻辑或数据转换。

以下是更新后的用户实体,提供了一些如何使用它的示例

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;
use CodeIgniter\I18n\Time;

class User extends Entity
{
    public function setPassword(string $pass)
    {
        $this->attributes['password'] = password_hash($pass, PASSWORD_BCRYPT);

        return $this;
    }

    public function setCreatedAt(string $dateString)
    {
        $this->attributes['created_at'] = new Time($dateString, 'UTC');

        return $this;
    }

    public function getCreatedAt(string $format = 'Y-m-d H:i:s')
    {
        // Convert to CodeIgniter\I18n\Time object
        $this->attributes['created_at'] = $this->mutateDate($this->attributes['created_at']);

        $timezone = $this->timezone ?? app_timezone();

        $this->attributes['created_at']->setTimezone($timezone);

        return $this->attributes['created_at']->format($format);
    }
}

首先要注意的是我们添加的方法名称。对于每个方法,该类都希望将 snake_case 列名称转换为 PascalCase,并以 setget 为前缀。然后,无论何时使用直接语法(即 $user->email)设置或检索类属性,这些方法都会自动调用。除非您希望从其他类访问这些方法,否则这些方法不必是公共的。例如,created_at 类属性将通过 setCreatedAt()getCreatedAt() 方法进行访问。

注意

这仅在尝试从类外部访问属性时才有效。类内部的任何方法都必须直接调用 setX()getX() 方法。

setPassword() 方法中,我们确保密码始终被哈希。

setCreatedAt() 中,我们将从模型接收到的字符串转换为 DateTime 对象,确保我们的时区是 UTC,以便我们可以轻松转换查看者的当前时区。在 getCreatedAt() 中,它将时间转换为应用程序当前时区中的格式化字符串。

虽然相当简单,但这些示例表明使用实体类可以提供一种非常灵活的方式来强制执行业务逻辑并创建易于使用的对象。

<?php

// Auto-hash the password - both do the same thing
$user->password = 'my great password';
$user->setPassword('my great password');

特殊 Getter/Setter

4.4.0 版中的新增功能。

例如,如果您的实体的父类已经定义了 getParent() 方法,并且您的实体还具有名为 parent 的列,当您尝试向实体类中的 getParent() 方法添加业务逻辑时,该方法已被定义。

在这种情况下,您可以使用特殊 getter/setter。代替 getX()/setX(),设置 _getX()/_setX()

在上面的示例中,如果您的实体具有 _getParent() 方法,那么当您获取 $entity->parent 时将使用该方法,当您设置 $entity->parent 时将使用 _setParent() 方法。

数据映射

在职业生涯的许多时刻,您会遇到应用程序的使用已发生变化的情况,并且数据库中的原始列名已不再有意义。或者,您发现您的编码风格更喜欢 camelCase 类属性,但您的数据库架构需要 snake_case 名称。使用实体类的 data mapping 特性可以轻松处理这些情况。

例如,假设您在整个应用程序中都使用了简化的用户实体

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    protected $attributes = [
        'id'         => null,
        'name'       => null, // Represents a username
        'email'      => null,
        'password'   => null,
        'created_at' => null,
        'updated_at' => null,
    ];
}

您的老板来找您,并说现在没有人再使用用户名了,因此您将切换为仅使用电子邮件进行登录。但他们确实希望对应用程序进行一些个性化设置,因此他们希望您将 name 字段更改为现在表示用户的全名,而不是像当前那样的用户名。为了保持整洁并确保数据库中的内容继续有意义,您编写了一个迁移,将 name 字段更名为 full_name 以提高清晰度。

忽略此示例的牵强附会,我们现在有两种选择来修复用户类。我们可以将类属性从 $name 修改为 $full_name,但这需要在整个应用程序中进行更改。相反,我们可以简单地将数据库中的 full_name 列映射到 $name 属性,并完成实体更改

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    protected $attributes = [
        'id'         => null,
        'full_name'  => null, // In the $attributes, the key is the db column name
        'email'      => null,
        'password'   => null,
        'created_at' => null,
        'updated_at' => null,
    ];

    protected $datamap = [
        // property_name => db_column_name
        'name' => 'full_name',
    ];
}

通过将我们的新数据库名称添加到 $datamap 数组中,我们可以告诉类数据库列应通过哪个类属性进行访问。数组的键是要映射到的类属性,而数组中的值是数据库中列的名称。

在此示例中,当模型在用户类上设置 full_name 字段时,它实际上将该值分配给类的 $name 属性,因此可以通过 $user->name 进行设置和检索。该值仍可以通过原始 $user->full_name 访问,这也是模型将数据取回并将其保存到数据库所需的。但是,unset()isset() 仅适用于映射的属性 $user->name,而不适用于数据库列名 $user->full_name

注意

当您使用数据映射时,必须为数据库列名定义 set*()get*() 方法。在此示例中,您必须定义 setFullName()getFullName()

Mutators

Date Mutators

默认情况下,Entity 类会将名为 created_atupdated_atdeleted_at 的字段转换为 Time 实例,无论何时设置或检索它们。Time 类以不可变、本地化的方式提供大量有用的方法。

您可以通过将名称添加到 $dates 属性来定义自动转换哪些属性

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    protected $dates = ['created_at', 'updated_at', 'deleted_at'];
}

现在,当设置任何这些属性时,它们将转换为 Time 实例,使用应用程序的当前时区,如 app/Config/App.php 中所设置

<?php

$user = new \App\Entities\User();

// Converted to Time instance
$user->created_at = 'April 15, 2017 10:30:00';

// Can now use any Time methods:
echo $user->created_at->humanize();
echo $user->created_at->setTimezone('Europe/London')->toDateString();

Property Casting

您可以使用 $casts 属性指定实体中的属性应转换为常见数据类型。此选项应为一个数组,其中键是类属性的名称,值是要转换到的数据类型。

属性转换影响读取(get)和写入(set),但某些类型仅影响读取(get)。

标量类型转换

属性可以转换为以下任何数据类型:integerfloatdoublestringbooleanobjectarraydatetimetimestampuriint-bool。在类型前面添加问号以将属性标记为可为空,即 ?string?integer

注意

int-bool 可自 v4.3.0 起使用。

例如,如果您有一个具有 is_banned 属性的 User 实体,则可以将其转换为布尔值

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    protected $casts = [
        'is_banned'          => 'boolean',
        'is_banned_nullable' => '?boolean',
    ];
}

数组/Json 转换

数组/Json 转换对于存储序列化数组或 json 的字段特别有用。当转换为

  • 一个数组,它们将自动反序列化,

  • 一个json,它们将自动设置为 json_decode($value, false) 的值,

  • 一个json 数组,它们将自动设置为 json_decode($value, true) 的值,

当你设置属性值时。与你可以将属性强制转换为的其他数据类型不同,

  • 数组强制转换类型将序列化,

  • jsonjson 数组强制转换将在

设置属性时对值使用 json_encode 函数

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    protected $casts = [
        'options'        => 'array',
        'options_object' => 'json',
        'options_array'  => 'json-array',
    ];
}
<?php

$user    = $userModel->find(15);
$options = $user->options;

$options['foo'] = 'bar';

$user->options = $options;
$userModel->save($user);

CSV 强制转换

如果你知道有一个简单值的平面数组,将它们编码为序列化或 JSON 字符串可能比原始结构更复杂。作为逗号分隔值 (CSV) 的强制转换是一个更简单的替代方案,将生成一个使用更少空间且更易于人类阅读的字符串

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class Widget extends Entity
{
    protected $casts = [
        'colors' => 'csv',
    ];
}

存储在数据库中为“red,yellow,green”

<?php

$widget->colors = ['red', 'yellow', 'green'];

注意

作为 CSV 的强制转换使用 PHP 的内部 implodeexplode 方法,并假定所有值都是字符串安全的且不包含逗号。对于更复杂的数据强制转换,请尝试 arrayjson

自定义强制转换

你可以定义自己的转换类型来获取和设置数据。

首先,你需要为你的类型创建一个处理程序类。假设该类将位于 app/Entities/Cast 目录中

<?php

namespace App\Entities\Cast;

use CodeIgniter\Entity\Cast\BaseCast;

// The class must inherit the CodeIgniter\Entity\Cast\BaseCast class
class CastBase64 extends BaseCast
{
    public static function get($value, array $params = [])
    {
        return base64_decode($value, true);
    }

    public static function set($value, array $params = [])
    {
        return base64_encode($value);
    }
}

现在你需要注册它

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class MyEntity extends Entity
{
    // Specifying the type for the field
    protected $casts = [
        'key' => 'base64',
    ];

    // Bind the type to the handler
    protected $castHandlers = [
        'base64' => Cast\CastBase64::class,
    ];
}

// ...

$entity->key = 'test'; // dGVzdA==
echo $entity->key;     // test

如果你不需要在获取或设置值时更改值。那么只需不实现适当的方法

<?php

namespace App\Entities\Cast;

use CodeIgniter\Entity\Cast\BaseCast;

class CastBase64 extends BaseCast
{
    public static function get($value, array $params = [])
    {
        return base64_decode($value, true);
    }
}

参数

在某些情况下,一种类型不够。在这种情况下,你可以使用其他参数。其他参数用方括号表示,并用逗号列出,如 type[param1, param2]

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class MyEntity extends Entity
{
    // Defining a type with parameters
    protected $casts = [
        'some_attribute' => 'class[App\SomeClass, param2, param3]',
    ];

    // Bind the type to the handler
    protected $castHandlers = [
        'class' => 'SomeHandler',
    ];
}
<?php

namespace App\Entities\Cast;

use CodeIgniter\Entity\Cast\BaseCast;

class SomeHandler extends BaseCast
{
    public static function get($value, array $params = [])
    {
        var_dump($params);
        /*
         * Output:
         * array(3) {
         *   [0]=>
         *   string(13) "App\SomeClass"
         *   [1]=>
         *   string(6) "param2"
         *   [2]=>
         *   string(6) "param3"
         * }
         */
    }
}

注意

如果强制转换类型标记为可空 ?bool 且传递的值不为 null,则带有值 nullable 的参数将传递给强制转换类型处理程序。如果强制转换类型具有预定义的参数,则 nullable 将添加到列表的末尾。

检查已更改的属性

您可以检查实体属性自创建以来是否已更改。唯一的参数是要检查的属性的名称

<?php

$user = new \App\Entities\User();
$user->hasChanged('name'); // false

$user->name = 'Fred';
$user->hasChanged('name'); // true

或要检查整个实体的更改值,请省略参数

<?php

$user->hasChanged(); // true