实体

class Cake\ORM\Entity

虽然 表对象 代表并提供对对象集合的访问,但实体代表应用程序中的单个行或域对象。实体包含用于操作和访问其包含的数据的方法。字段也可以作为对象的属性访问。

每次迭代表对象返回的查询实例或调用查询实例的 all()first() 方法时,都会为您创建实体。

创建实体类

您不需要创建实体类即可开始使用 CakePHP 中的 ORM。但是,如果您希望在实体中使用自定义逻辑,您需要创建类。按照惯例,实体类位于 **src/Model/Entity/** 中。如果我们的应用程序有一个 articles 表,我们可以创建以下实体

// src/Model/Entity/Article.php
namespace App\Model\Entity;

use Cake\ORM\Entity;

class Article extends Entity
{
}

现在这个实体并没有做太多事情。但是,当我们从 articles 表中加载数据时,我们将获得此类的实例。

注意

如果您没有定义实体类,CakePHP 将使用基本的 Entity 类。

创建实体

实体可以直接实例化

use App\Model\Entity\Article;

$article = new Article();

在实例化实体时,您可以将包含您要存储在其中的数据的字段传递给它

use App\Model\Entity\Article;

$article = new Article([
    'id' => 1,
    'title' => 'New Article',
    'created' => new DateTime('now')
]);

获取新实体的首选方法是使用 newEmptyEntity() 方法来自 Table 对象

use Cake\ORM\Locator\LocatorAwareTrait;

$article = $this->fetchTable('Articles')->newEmptyEntity();

$article = $this->fetchTable('Articles')->newEntity([
    'id' => 1,
    'title' => 'New Article',
    'created' => new DateTime('now')
]);

$article 将是 App\Model\Entity\Article 的实例,如果您没有创建 Article 类,则将回退到 Cake\ORM\Entity 实例。

注意

在 CakePHP 4.3 之前,您需要使用 $this->getTableLocator->get('Articles') 来获取表实例。

访问实体数据

实体提供了几种访问其包含数据的方案。最常见的是,您将使用对象表示法访问实体中的数据

use App\Model\Entity\Article;

$article = new Article;
$article->title = 'This is my first post';
echo $article->title;

您也可以使用 get()set() 方法。

Cake\ORM\Entity::set($field, $value = null, array $options = [])
Cake\ORM\Entity::get($field)

例如

$article->set('title', 'This is my first post');
echo $article->get('title');

当使用 set() 时,您可以使用数组一次更新多个字段

$article->set([
    'title' => 'My first post',
    'body' => 'It is the best ever!'
]);

警告

使用请求数据更新实体时,您应该配置哪些字段可以使用批量赋值。

您可以使用 has() 检查实体中是否定义了字段

$article = new Article([
    'title' => 'First post',
    'user_id' => null
]);
$article->has('title'); // true
$article->has('user_id'); // true
$article->has('undefined'); // false

has() 方法将在定义字段时返回 true。您可以使用 isEmpty()hasValue() 检查字段是否包含“非空”值

$article = new Article([
    'title' => 'First post',
    'user_id' => null,
    'text' => '',
    'links' => [],
]);
$article->has('title'); // true
$article->isEmpty('title');  // false
$article->hasValue('title'); // true

$article->has('user_id'); // true
$article->isEmpty('user_id');  // true
$article->hasValue('user_id'); // false

$article->has('text'); // true
$article->isEmpty('text');  // true
$article->hasValue('text'); // false

$article->has('links'); // true
$article->isEmpty('links');  // true
$article->hasValue('links'); // false

如果您经常部分加载实体,您应该启用严格属性访问行为,以确保您没有使用未加载的属性。您可以在每个实体的基础上启用此行为

$article->requireFieldPresence();

启用后,访问未定义的属性将引发 CakeORMMissingPropertyException

访问器和修改器

除了简单的 get/set 接口之外,实体还允许您提供访问器和修改器方法。这些方法让您自定义字段的读取或设置方式。

访问器

访问器让您自定义字段的读取方式。它们使用 _get(FieldName) 的约定,其中 (FieldName) 是字段名称的驼峰式版本(多个单词合并成一个单词,每个单词的首字母大写)。

它们接收存储在 _fields 数组中的基本值作为其唯一参数。例如

namespace App\Model\Entity;

use Cake\ORM\Entity;

class Article extends Entity
{
    protected function _getTitle($title)
    {
        return strtoupper($title);
    }
}

上面的示例每次读取 title 字段的值时,都会将其转换为大写版本。它将在通过以下两种方式之一获取字段时运行

echo $article->title; // returns FOO instead of foo
echo $article->get('title'); // returns FOO instead of foo

注意

访问器中的代码在每次引用字段时都会执行。如果您在访问器中执行资源密集型操作(例如:$myEntityProp = $entity->my_property),可以使用局部变量对其进行缓存。

警告

访问器将在保存实体时使用,因此在定义用于格式化数据的方法时要小心,因为格式化的数据将被持久化。

修改器

您可以通过定义修改器来自定义字段的设置方式。它们使用 _set(FieldName) 的约定,其中 (FieldName) 是字段名称的驼峰式版本。

修改器应始终返回应存储在字段中的值。您还可以使用修改器设置其他字段。这样做时,请务必不要引入任何循环,因为 CakePHP 不会阻止无限循环修改器方法。例如

namespace App\Model\Entity;

use Cake\ORM\Entity;
use Cake\Utility\Text;

class Article extends Entity
{
    protected function _setTitle($title)
    {
        $this->slug = Text::slug($title);

        return strtoupper($title);
    }
}

上面的示例做了两件事:它将给定值的修改版本存储在 slug 字段中,并将大写版本存储在 title 字段中。它将在通过以下两种方式之一设置字段时运行

$user->title = 'foo'; // sets slug field and stores FOO instead of foo
$user->set('title', 'foo'); // sets slug field and stores FOO instead of foo

警告

访问器也会在实体持久化到数据库之前运行。如果您想转换字段但不持久化转换,建议使用虚拟字段,因为它们不会被持久化。

创建虚拟字段

通过定义访问器,您可以提供对实际上不存在的字段的访问。例如,如果您的用户表具有 first_namelast_name,您可以为全名创建一个方法

namespace App\Model\Entity;

use Cake\ORM\Entity;

class User extends Entity
{
    protected function _getFullName()
    {
        return $this->first_name . '  ' . $this->last_name;
    }
}

您可以像它们存在于实体上一样访问虚拟字段。属性名称将是方法的小写下划线版本(full_name

echo $user->full_name;
echo $user->get('full_name');

请记住,虚拟字段不能在查找中使用。如果您希望它们成为实体的 JSON 或数组表示的一部分,请参阅 公开虚拟字段

检查实体是否已被修改

Cake\ORM\Entity::dirty($field = null, $dirty = null)

您可能希望根据实体中的字段是否发生更改来使代码有条件地执行。例如,您可能只想在字段更改时验证字段

// See if the title has been modified.
$article->isDirty('title');

您也可以将字段标记为已修改。当追加到数组字段中时,这非常有用,因为这不会自动将字段标记为脏字段,只有完全交换才会标记为脏字段。

// Add a comment and mark the field as changed.
$article->comments[] = $newComment;
$article->setDirty('comments', true);

此外,您还可以使用 getOriginal() 方法,根据原始字段值编写条件代码。此方法将返回字段的原始值(如果它已被修改)或其实际值。

您还可以检查实体中任何字段的更改

// See if the entity has changed
$article->isDirty();

要从实体中的字段中删除脏标记,可以使用 clean() 方法

$article->clean();

创建新实体时,可以通过传递额外选项来避免字段被标记为脏字段

$article = new Article(['title' => 'New Article'], ['markClean' => true]);

要获取 Entity 的所有脏字段的列表,您可以调用

$dirtyFields = $entity->getDirty();

验证错误

保存实体后,任何验证错误都将存储在实体本身中。您可以使用 getErrors()getError()hasErrors() 方法访问任何验证错误

// Get all the errors
$errors = $user->getErrors();

// Get the errors for a single field.
$errors = $user->getError('password');

// Does the entity or any nested entity have an error.
$user->hasErrors();

// Does only the root entity have an error
$user->hasErrors(false);

setErrors()setError() 方法也可以用来设置实体的错误,这使得测试使用错误消息的代码变得更容易

$user->setError('password', ['Password is required']);
$user->setErrors([
    'password' => ['Password is required'],
    'username' => ['Username is required']
]);

批量赋值

虽然将字段批量设置到实体中既简单又方便,但它会造成重大的安全问题。从请求中批量分配用户数据到实体中,允许用户修改任何和所有列。当使用匿名实体类或使用Bake Console创建实体类时,CakePHP 不会防止批量赋值。

_accessible 属性允许您提供字段映射,以及它们是否可以进行批量赋值。值 truefalse 表示字段是否可以或不能进行批量赋值

namespace App\Model\Entity;

use Cake\ORM\Entity;

class Article extends Entity
{
    protected array $_accessible = [
        'title' => true,
        'body' => true
    ];
}

除了具体字段之外,还有一个特殊的 * 字段,它定义了如果没有具体命名字段时的回退行为

namespace App\Model\Entity;

use Cake\ORM\Entity;

class Article extends Entity
{
    protected array $_accessible = [
        'title' => true,
        'body' => true,
        '*' => false,
    ];
}

注意

如果没有定义 * 字段,它将默认为 false

避免批量赋值保护

当使用 new 关键字创建新实体时,您可以告诉它不要保护自己免受批量赋值

use App\Model\Entity\Article;

$article = new Article(['id' => 1, 'title' => 'Foo'], ['guard' => false]);

在运行时修改受保护字段

您可以使用 setAccess() 方法在运行时修改受保护字段列表

// Make user_id accessible.
$article->setAccess('user_id', true);

// Make title guarded.
$article->setAccess('title', false);

注意

修改可访问字段只影响调用该方法的实例。

当在 Table 对象中使用 newEntity()patchEntity() 方法时,您可以使用选项自定义批量赋值保护。有关更多信息,请参阅更改可访问字段部分。

绕过字段保护

在某些情况下,您可能希望允许对受保护字段进行批量赋值

$article->set($fields, ['guard' => false]);

通过将 guard 选项设置为 false,您可以忽略对 set() 的单个调用的可访问字段列表。

检查实体是否已持久化

通常需要知道实体是否表示已存在于数据库中的行。在这些情况下,使用 isNew() 方法

if (!$article->isNew()) {
    echo 'This article was saved already!';
}

如果您确定实体已持久化,可以使用 setNew()

$article->setNew(false);

$article->setNew(true);

延迟加载关联

虽然急切加载关联通常是访问关联的最有效方法,但有时您可能需要延迟加载关联数据。在我们深入了解如何延迟加载关联之前,我们应该讨论急切加载和延迟加载关联之间的区别

急切加载

急切加载使用联接(在可能的情况下)以尽可能少的查询从数据库中获取数据。当需要单独的查询时,例如在 HasMany 关联的情况下,会发出单个查询来获取当前对象集中所有关联的数据。

延迟加载

延迟加载推迟加载关联数据,直到绝对需要。虽然这可以节省 CPU 时间,因为可能未使用的數據沒有被水化成对象,但这会导致向数据库发出更多查询。例如,遍历一组文章及其评论将经常发出 N 个查询,其中 N 是正在遍历的文章数。

虽然延迟加载没有包含在 CakePHP 的 ORM 中,但您可以使用社区插件之一来实现。我们推荐LazyLoad 插件

将插件添加到实体后,您将能够执行以下操作

$article = $this->Articles->findById($id);

// The comments property was lazy loaded
foreach ($article->comments as $comment) {
    echo $comment->body;
}

使用 Traits 创建可重用代码

您可能会发现自己在多个实体类中需要相同的逻辑。PHP 的 Traits 非常适合这种情况。您可以将应用程序的 Traits 放置在 **src/Model/Entity** 中。按照约定,CakePHP 中的 Traits 以 Trait 结尾,以便它们可以与类或接口区分开来。Traits 通常是对行为的很好的补充,允许您为表和实体对象提供功能。

例如,如果我们有 SoftDeletable 插件,它可以提供一个 Trait。此 Trait 可以提供用于将实体标记为“已删除”的方法,softDelete 方法可以由 Trait 提供

// SoftDelete/Model/Entity/SoftDeleteTrait.php

namespace SoftDelete\Model\Entity;

trait SoftDeleteTrait
{
    public function softDelete()
    {
        $this->set('deleted', true);
    }
}

然后,您可以在实体类中通过导入和包含它来使用此 Trait

namespace App\Model\Entity;

use Cake\ORM\Entity;
use SoftDelete\Model\Entity\SoftDeleteTrait;

class Article extends Entity
{
    use SoftDeleteTrait;
}

转换为数组/JSON

在构建 API 时,您可能经常需要将实体转换为数组或 JSON 数据。CakePHP 使这变得简单

// Get an array.
// Associations will be converted with toArray() as well.
$array = $user->toArray();

// Convert to JSON
// Associations will be converted with jsonSerialize hook as well.
$json = json_encode($user);

将实体转换为 JSON 时,将应用虚拟和隐藏字段列表。实体也会递归地转换为 JSON。这意味着,如果您急切加载实体及其关联,CakePHP 将正确处理将关联数据转换为正确格式。

公开虚拟字段

默认情况下,虚拟字段在将实体转换为数组或 JSON 时不会导出。为了公开虚拟字段,您需要使它们可见。在定义实体类时,您可以提供应公开的虚拟字段列表

namespace App\Model\Entity;

use Cake\ORM\Entity;

class User extends Entity
{
    protected $_virtual = ['full_name'];
}

此列表可以在运行时使用 setVirtual() 方法进行修改

$user->setVirtual(['full_name', 'is_admin']);

隐藏字段

通常有您不希望在 JSON 或数组格式中导出的字段。例如,公开密码哈希或帐户恢复问题通常是不明智的。在定义实体类时,定义哪些字段应隐藏

namespace App\Model\Entity;

use Cake\ORM\Entity;

class User extends Entity
{
    protected $_hidden = ['password'];
}

此列表可以在运行时使用 setHidden() 方法进行修改

$user->setHidden(['password', 'recovery_question']);

存储复杂类型

实体上的访问器和修改器方法不打算包含来自数据库的复杂数据的序列化和反序列化逻辑。请参阅保存复杂类型部分,以了解您的应用程序如何存储更复杂的数据类型,如数组和对象。