行为

行为是组织和启用模型层逻辑水平重用的一种方式。从概念上讲,它们类似于特性。但是,行为被实现为单独的类。这使得它们能够连接到模型发出的生命周期回调,同时提供类似特性的功能。

行为为打包跨多个模型通用的行为提供了一种便捷的方式。例如,CakePHP 包含一个 TimestampBehavior。许多模型将需要时间戳字段,而管理这些字段的逻辑并不特定于任何一个模型。正是这些场景非常适合使用行为。

使用行为

行为提供了一种方法来创建与表类相关的水平可重用逻辑片段。您可能想知道为什么行为是普通的类而不是特性。主要原因是事件监听器。虽然特性允许可重用逻辑片段,但它们会使事件绑定复杂化。

要将行为添加到您的表中,您可以调用 addBehavior() 方法。通常,最好的做法是在 initialize() 方法中执行此操作

namespace App\Model\Table;

use Cake\ORM\Table;

class ArticlesTable extends Table
{
    public function initialize(array $config): void
    {
        $this->addBehavior('Timestamp');
    }
}

与关联一样,您可以使用 插件语法 并提供其他配置选项

namespace App\Model\Table;

use Cake\ORM\Table;

class ArticlesTable extends Table
{
    public function initialize(array $config): void
    {
        $this->addBehavior('Timestamp', [
            'events' => [
                'Model.beforeSave' => [
                    'created_at' => 'new',
                    'modified_at' => 'always'
                ]
            ]
        ]);
    }
}

核心行为

创建行为

在以下示例中,我们将创建一个非常简单的 SluggableBehavior。此行为将允许我们使用 Text::slug() 的结果填充一个 slug 字段,该字段基于另一个字段。

在创建行为之前,我们应该了解行为的约定

  • 行为文件位于 src/Model/Behavior 中,或 MyPlugin\Model\Behavior 中。

  • 行为类应位于 App\Model\Behavior 命名空间中,或 MyPlugin\Model\Behavior 命名空间中。

  • 行为类名以 Behavior 结尾。

  • 行为扩展 Cake\ORM\Behavior

要创建可剥离行为。将以下内容放入 src/Model/Behavior/SluggableBehavior.php

namespace App\Model\Behavior;

use Cake\ORM\Behavior;

class SluggableBehavior extends Behavior
{
}

与表类似,行为也有一个 initialize() 钩子,您可以在其中放置行为的初始化代码,如果需要的话

public function initialize(array $config): void
{
    // Some initialization code here
}

现在我们可以将此行为添加到我们的一个表类中。在本例中,我们将使用 ArticlesTable,因为文章通常具有 slug 属性以创建友好的 URL

namespace App\Model\Table;

use Cake\ORM\Table;

class ArticlesTable extends Table
{
    public function initialize(array $config): void
    {
        $this->addBehavior('Sluggable');
    }
}

我们的新行为现在还没有做太多事情。接下来,我们将添加一个混合方法和一个事件监听器,以便在我们保存实体时,可以自动对字段进行剥离。

定义混合方法

行为上定义的任何公有方法都将作为“混合”方法添加到它附加到的表对象上。如果您附加两个提供相同方法的行为,则会引发异常。如果行为提供了与表类相同的方法,则无法从表中调用行为方法。行为混合方法将接收与提供给表的完全相同的参数。例如,如果我们的 SluggableBehavior 定义了以下方法

public function slug($value)
{
    return Text::slug($value, $this->_config['replacement']);
}

可以使用以下方式调用它

$slug = $articles->slug('My article name');

限制或重命名公开的混合方法

在创建行为时,可能存在您不想将公有方法公开为混合方法的情况。在这些情况下,您可以使用 implementedMethods 配置键来重命名或排除混合方法。例如,如果我们想为我们的 slug() 方法添加前缀,我们可以执行以下操作

protected $_defaultConfig = [
    'implementedMethods' => [
        'superSlug' => 'slug',
    ]
];

应用此配置将使 slug() 不可调用,但它将向表添加一个 superSlug() 混合方法。值得注意的是,如果我们的行为实现了其他公有方法,它们将不会作为具有上述配置的混合方法可用。

由于公开的方法是由配置决定的,因此您也可以在将行为添加到表时重命名/删除混合方法。例如

// In a table's initialize() method.
$this->addBehavior('Sluggable', [
    'implementedMethods' => [
        'superSlug' => 'slug',
    ]
]);

定义事件监听器

现在我们的行为有了一个混合方法来剥离字段,我们可以实现一个回调监听器,以便在保存实体时自动剥离字段。我们还将修改我们的 slug 方法以接受实体而不是仅接受普通值。我们的行为现在应该看起来像这样

namespace App\Model\Behavior;

use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query\SelectQuery;
use Cake\Utility\Text;

class SluggableBehavior extends Behavior
{
    protected array $_defaultConfig = [
        'field' => 'title',
        'slug' => 'slug',
        'replacement' => '-',
    ];

    public function slug(EntityInterface $entity)
    {
        $config = $this->getConfig();
        $value = $entity->get($config['field']);
        $entity->set($config['slug'], Text::slug($value, $config['replacement']));
    }

    public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
    {
        $this->slug($entity);
    }

}

上面的代码展示了行为的一些有趣功能

  • 行为可以通过定义遵循 生命周期回调 约定的方法来定义回调方法。

  • 行为可以定义一个默认配置属性。此属性在将行为附加到表时与覆盖项合并。

要阻止保存继续,只需在回调中停止事件传播

public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
    if (...) {
        $event->stopPropagation();
        $event->setResult(false);

        return;
    }
    $this->slug($entity);
}

或者,您可以从回调中返回 false。这与停止事件传播的效果相同。

定义查找器

现在我们能够保存具有 slug 值的文章,我们应该实现一个查找器方法,以便我们可以按其 slug 检索文章。行为查找器方法使用与 自定义查找器方法 相同的约定。我们的 find('slug') 方法将如下所示

public function findSlug(SelectQuery $query, string $slug): SelectQuery
{
    return $query->where(['slug' => $slug]);
}

一旦我们的行为有了上述方法,我们就可以调用它

$article = $articles->find('slug', slug: $value)->first();

限制或重命名公开的查找器方法

在创建行为时,可能存在您不想公开查找器方法,或者您需要重命名查找器以避免重复方法的情况。在这些情况下,您可以使用 implementedFinders 配置键来重命名或排除查找器方法。例如,如果我们想重命名我们的 find(slug) 方法,我们可以执行以下操作

protected array $_defaultConfig = [
    'implementedFinders' => [
        'slugged' => 'findSlug',
    ]
];

应用此配置将使 find('slug') 触发错误。但是,它将使 find('slugged') 可用。值得注意的是,如果我们的行为实现了其他查找器方法,它们将不可用,因为它们未包含在配置中。

由于公开的方法是由配置决定的,因此您也可以在将行为添加到表时重命名/删除查找器方法。例如

// In a table's initialize() method.
$this->addBehavior('Sluggable', [
    'implementedFinders' => [
        'slugged' => 'findSlug',
    ]
]);

将请求数据转换为实体属性

行为可以通过实现 Cake\ORM\PropertyMarshalInterface 来定义其提供的自定义字段如何被封送。此接口要求实现一个单一方法

public function buildMarshalMap($marshaller, $map, $options)
{
    return [
        'custom_behavior_field' => function ($value, $entity) {
            // Transform the value as necessary
            return $value . '123';
        }
    ];
}

您可能想参考 TranslateBehavior,它对该接口有一个非平凡的实现。

删除已加载的行为

要从您的表中删除行为,您可以调用 removeBehavior() 方法

// Remove the loaded behavior
$this->removeBehavior('Sluggable');

访问已加载的行为

将行为附加到您的 Table 实例后,您可以内省已加载的行为,或使用 BehaviorRegistry 访问特定行为

// See which behaviors are loaded
$table->behaviors()->loaded();

// Check if a specific behavior is loaded.
// Remember to omit plugin prefixes.
$table->behaviors()->has('CounterCache');

// Get a loaded behavior
// Remember to omit plugin prefixes
$table->behaviors()->get('CounterCache');

重新配置已加载的行为

要修改已加载行为的配置,您可以将 BehaviorRegistry::get 命令与 config 命令组合使用,该命令由 InstanceConfigTrait 特性提供。

例如,如果一个父类(如 AppTable)加载了 Timestamp 行为,您可以执行以下操作来添加、修改或删除行为的配置。在本例中,我们将添加一个我们希望 Timestamp 响应的事件

namespace App\Model\Table;

use App\Model\Table\AppTable; // similar to AppController

class UsersTable extends AppTable
{
    public function initialize(array $options): void
    {
        parent::initialize($options);

        // For example, if our parent calls $this->addBehavior('Timestamp')
        // and we want to add an additional event
        if ($this->behaviors()->has('Timestamp')) {
            $this->behaviors()->get('Timestamp')->setConfig([
                'events' => [
                    'Users.login' => [
                        'last_login' => 'always'
                    ],
                ],
            ]);
        }
    }
}