行为是组织和启用模型层逻辑水平重用的一种方式。从概念上讲,它们类似于特性。但是,行为被实现为单独的类。这使得它们能够连接到模型发出的生命周期回调,同时提供类似特性的功能。
行为为打包跨多个模型通用的行为提供了一种便捷的方式。例如,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'
],
],
]);
}
}
}