如何针对自身以及数据库中的现有行验证多个实体?
How to validate multiple entities against themselves as well as against existing rows in the database?
我有一个应用程序需要验证营业时间以确保它们在 CakePHP 3 中不重叠。所有记录都可以在一个表单中更改。数据看起来有点像这样:
id
day
opening_time
closing_time
1
1
08:00:00
13:00:00
2
1
16:00:00
22:00:00
现在,当我将第一个 opening_time 更改为 17:00 时,这将是无效的,因为它会与第二行重叠。但是当我以相同的形式将第二个 opening_time 更改为 18:00 时,它应该是有效的。
我尝试使用构建规则:
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->existsIn(['store_id'], 'Stores'));
$rules->add(
function (BusinessHour $entity, array $options): bool {
$conditions = [
'id !=' => $entity->id,
'store_id' => $entity->store_id,
'day' => $entity->day,
'OR' => [
[
'opening_time <=' => $entity->opening_time,
'closing_time >=' => $entity->opening_time,
],
[
'opening_time <=' => $entity->closing_time,
'closing_time >=' => $entity->closing_time,
],
[
'opening_time >=' => $entity->opening_time,
'opening_time <=' => $entity->closing_time,
],
[
'closing_time >=' => $entity->opening_time,
'closing_time <=' => $entity->closing_time,
]
]
];
return !$options['repository']->exists($conditions);
},
'overlapping',
[
'errorField' => 'opening_time',
'message' => __('Business hours may not overlap.'),
]
);
return $rules;
}
但它会根据数据库中的数据检查第一条记录并标记为无效,即使第二行的更改会使它有效。例如,当数据库中的数据如上所述并且我有以下 post 数据时,它应该有效但不是。
$data['business_hours'] = [
(int) 0 => [
'day' => '0',
'opening_time' => [
'hour' => '16',
'minute' => '30'
],
'closing_time' => [
'hour' => '17',
'minute' => '00'
]
],
(int) 1 => [
'day' => '0',
'opening_time' => [
'hour' => '18',
'minute' => '00'
],
'closing_time' => [
'hour' => '20',
'minute' => '00'
]
],
];
我该如何处理?
从逻辑上讲,您一次只能保存一个实体,不仅在应用程序级别,而且在 DBMS 级别,所以告诉您数据无效的规则是正确的,因为它只验证一个实体一次。
我认为你要么需要某种状态来处理这个,要么你需要混合使用验证和应用程序规则,即你还需要验证无状态数据作为 stateful/persisted 数据,不仅非持久化数据集中的所有时间都需要相互有效,它们还需要与数据库中已经持久化的数据有效。
有很多空间可以讨论如何最好地解决这个问题,我想,应用程序规则中还没有允许验证多个实体的接口,所以不管你如何解决这个问题,它可能是某种 compromise/workaround 一种或另一种方式。
因为这最终是关于数据库中的数据,所以所有验证都应该在事务中发生,所以我想最好将事情保留在应用程序规则中。我能想到的一种肮脏的方法是将所有实体的 ID 传递到保存过程中,并且在您的规则中排除所有尚未检查(因此保存)的 ID yet,这样后续检查将 运行 针对之前已经保留的实体,这意味着最终您将检查所有记录的有效性。
这是一个例子 - 我不知道你的关联设置,所以我假设你正在保存 Stores
和关联的 BusinessHours
,确切的设置并不重要, 不过,它应该只显示如何构建和传递自定义选项:
// ...
$store = $this->Stores->patchEntity($store, $this->request->getData());
$businessHourIds = new \ArrayObject(
collection($store->business_hours)
->extract('id')
->filter()
->indexBy(function ($value) {
return $value;
})
->toArray()
);
// $businessHourIds would represent an array like this, where both
// the key and the value would hold the ID: [1 => 1, 2 => 2, 3 => 3, ...]
if ($this->Stores->save($sore, ['businessHourIds' => $businessHourIds])) {
// ...
}
// ...
它是一个 ArrayObject
实例很重要,这样它的状态将在每个营业时间实体的多个不同 save()
调用中持续存在。然后,您的规则可以按照以下方式执行某些操作:
function (BusinessHour $entity, array $options): bool {
if (!isset($options['businessHourIds'])) {
return false;
}
// If the entity isn't new, its ID must be present in the IDs list
// so that it can be excluded in the exists check
if (
!$entity->isNew() &&
!isset($options['businessHourIds'][$entity->id]
) {
return false;
}
// the current entries opening time must be smaller than its closing time
if ($entity->opening_time >= $entity->closing_time) {
return false;
}
$conditions = [
'id NOT IN' => (array)$options['businessHourIds'],
'store_id' => $entity->store_id,
'day' => $entity->day,
'closing_time >' => $entity->opening_time,
'opening_time <' => $entity->closing_time,
];
// remove the current entity ID from the list, so that for the next
// check it is not being excluded, meaning that all following business
// hours will have to be valid according to this entity's data
unset($options['businessHourIds'][$entity->id]);
return !$options['repository']->exists($conditions);
}
所以这是未经测试的,理想情况下它会起作用,但它主要是为了说明我在说什么。另请注意,我减少了 open/closing 时间条件,假设关闭时间必须大于打开时间,此检查理论上应该涵盖所有可能的重叠。
我有一个应用程序需要验证营业时间以确保它们在 CakePHP 3 中不重叠。所有记录都可以在一个表单中更改。数据看起来有点像这样:
id | day | opening_time | closing_time |
---|---|---|---|
1 | 1 | 08:00:00 | 13:00:00 |
2 | 1 | 16:00:00 | 22:00:00 |
现在,当我将第一个 opening_time 更改为 17:00 时,这将是无效的,因为它会与第二行重叠。但是当我以相同的形式将第二个 opening_time 更改为 18:00 时,它应该是有效的。
我尝试使用构建规则:
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->existsIn(['store_id'], 'Stores'));
$rules->add(
function (BusinessHour $entity, array $options): bool {
$conditions = [
'id !=' => $entity->id,
'store_id' => $entity->store_id,
'day' => $entity->day,
'OR' => [
[
'opening_time <=' => $entity->opening_time,
'closing_time >=' => $entity->opening_time,
],
[
'opening_time <=' => $entity->closing_time,
'closing_time >=' => $entity->closing_time,
],
[
'opening_time >=' => $entity->opening_time,
'opening_time <=' => $entity->closing_time,
],
[
'closing_time >=' => $entity->opening_time,
'closing_time <=' => $entity->closing_time,
]
]
];
return !$options['repository']->exists($conditions);
},
'overlapping',
[
'errorField' => 'opening_time',
'message' => __('Business hours may not overlap.'),
]
);
return $rules;
}
但它会根据数据库中的数据检查第一条记录并标记为无效,即使第二行的更改会使它有效。例如,当数据库中的数据如上所述并且我有以下 post 数据时,它应该有效但不是。
$data['business_hours'] = [
(int) 0 => [
'day' => '0',
'opening_time' => [
'hour' => '16',
'minute' => '30'
],
'closing_time' => [
'hour' => '17',
'minute' => '00'
]
],
(int) 1 => [
'day' => '0',
'opening_time' => [
'hour' => '18',
'minute' => '00'
],
'closing_time' => [
'hour' => '20',
'minute' => '00'
]
],
];
我该如何处理?
从逻辑上讲,您一次只能保存一个实体,不仅在应用程序级别,而且在 DBMS 级别,所以告诉您数据无效的规则是正确的,因为它只验证一个实体一次。
我认为你要么需要某种状态来处理这个,要么你需要混合使用验证和应用程序规则,即你还需要验证无状态数据作为 stateful/persisted 数据,不仅非持久化数据集中的所有时间都需要相互有效,它们还需要与数据库中已经持久化的数据有效。
有很多空间可以讨论如何最好地解决这个问题,我想,应用程序规则中还没有允许验证多个实体的接口,所以不管你如何解决这个问题,它可能是某种 compromise/workaround 一种或另一种方式。
因为这最终是关于数据库中的数据,所以所有验证都应该在事务中发生,所以我想最好将事情保留在应用程序规则中。我能想到的一种肮脏的方法是将所有实体的 ID 传递到保存过程中,并且在您的规则中排除所有尚未检查(因此保存)的 ID yet,这样后续检查将 运行 针对之前已经保留的实体,这意味着最终您将检查所有记录的有效性。
这是一个例子 - 我不知道你的关联设置,所以我假设你正在保存 Stores
和关联的 BusinessHours
,确切的设置并不重要, 不过,它应该只显示如何构建和传递自定义选项:
// ...
$store = $this->Stores->patchEntity($store, $this->request->getData());
$businessHourIds = new \ArrayObject(
collection($store->business_hours)
->extract('id')
->filter()
->indexBy(function ($value) {
return $value;
})
->toArray()
);
// $businessHourIds would represent an array like this, where both
// the key and the value would hold the ID: [1 => 1, 2 => 2, 3 => 3, ...]
if ($this->Stores->save($sore, ['businessHourIds' => $businessHourIds])) {
// ...
}
// ...
它是一个 ArrayObject
实例很重要,这样它的状态将在每个营业时间实体的多个不同 save()
调用中持续存在。然后,您的规则可以按照以下方式执行某些操作:
function (BusinessHour $entity, array $options): bool {
if (!isset($options['businessHourIds'])) {
return false;
}
// If the entity isn't new, its ID must be present in the IDs list
// so that it can be excluded in the exists check
if (
!$entity->isNew() &&
!isset($options['businessHourIds'][$entity->id]
) {
return false;
}
// the current entries opening time must be smaller than its closing time
if ($entity->opening_time >= $entity->closing_time) {
return false;
}
$conditions = [
'id NOT IN' => (array)$options['businessHourIds'],
'store_id' => $entity->store_id,
'day' => $entity->day,
'closing_time >' => $entity->opening_time,
'opening_time <' => $entity->closing_time,
];
// remove the current entity ID from the list, so that for the next
// check it is not being excluded, meaning that all following business
// hours will have to be valid according to this entity's data
unset($options['businessHourIds'][$entity->id]);
return !$options['repository']->exists($conditions);
}
所以这是未经测试的,理想情况下它会起作用,但它主要是为了说明我在说什么。另请注意,我减少了 open/closing 时间条件,假设关闭时间必须大于打开时间,此检查理论上应该涵盖所有可能的重叠。