Field rules¶
A resource carries forty-odd fields, from five modules. Most sites answer most of them the same way for every resource, and answering them one resource at a time is both tedious and unreliable: the fortieth resource is set up by somebody who was not there for the first.
A field rule is one level's answer about one resource field: who may answer it, and with what value. Rules are made at three levels, on the same screen, reached as a Field rules tab:
| Level | Where | Applies to |
|---|---|---|
| Resource type | /admin/booking/resource-type/{id}/field-rules |
resources of that type |
| Tenant | /admin/booking/tenant/{id}/field-rules |
resources in that tenant |
| Site | /admin/booking/settings/site/field-rules |
every resource |
The nearest level that says anything wins: a resource type may tighten what its tenant left open, and a tenant what the site left open.
What a rule says¶
Two answers per field.
Wherever this field is asked, it is one of:
| Choice | On a form | Stored as |
|---|---|---|
| Inherit from … (No rule, at the site) | asked as usual | nothing; the level says nothing about it |
| Editable | asked as usual | shown |
| Shown, but not editable | the value in words, and the level that set it | read_only |
| Not asked at all | absent | hidden |
Editable is a rule like the others rather than a way of saying nothing: it stops the walk, so a tenant can reopen a field the site closed.
The value that applies answers every resource that says it inherits the field. On an editable field a resource may still answer differently and then keeps its own answer; on the other two, the rule's value is the answer. Left empty, the rule names no value, which on a field that is not asked at all means the feature is off.
How a resource says "inherit"¶
The resource's own row answers for it, and one of the values that row can hold
is the instruction to ask the level above. On the fields something follows,
which is every list of strings a rule may reach, that is a real stored value,
inherit, offered as a real choice, validated like any other, and the field's
default, so a resource starts out following its levels. Everywhere else an empty
field means the same thing, because a number and a duration cannot hold a word.
A field declares RuledField::FOLLOWS exactly when its readers ask
getEffectiveValue(), and its readers must ask for it to declare it. Doing one without
the other is a bug either way: the word inherit reaching a reader that
compares strings is a calendar asked to book "inherit" days, and a reader asking
for a field that carries no such value gets nothing to follow.
FieldRuleResolver::getEffectiveValue() is the one place that follows it. Every reader
asks that instead of walking the chain itself:
$settlement = $fieldRules->getEffectiveValue('yoyaku_resource.settlement', $resource);
Which means a level changing its mind reaches every resource still saying it inherits, and reaches no resource that answered for itself. Nothing is copied, so nothing goes stale.
The fields copied at creation instead¶
A few fields are copied onto a resource as it is created, and the resource
owns them from then on: venue, configuration, pinning, capacity, and the
money (price, collect_value, guarantee_amount, noshow_fee).
A venue that moved would leave a hall's seats in a building it is no longer in. The guarantee, the no-show fee and the amount collected online are read when they are applied rather than stamped, so one that moved would change the terms of what has already been booked.
The price is not that: a booking stamps its own unit_price as it is created,
and it is never restamped. It is copied for a plainer reason, that what a
resource charges is the resource's, and a site rule setting the price of every
resource at once is a pricing decision rather than a house rule.
A module declares which of its fields behave this way by setting
RuledField::COPIED_ON_CREATE on the definition it builds, beside
RuledField::FOLLOWS for the ones a rule follows instead, and
RuledField::FALLS_BACK_TO where the value an inheriting row ends up with is
the reader's own rather than the field's declared default.
What a rule may not do¶
A resource that already holds an answer of its own, different from what the rule names, keeps both its answer and its field: the rule is not applied to it at all, and the field stays on the form for an operator to settle. A rule may only take a field away where there is nothing to lose.
A required field cannot be hidden with no value in its place: an operator would
be left with a form that cannot be submitted and no word about which setting
broke it. FieldRuleResolver::refuse() says so where the rule is made, rather
than letting a save fail somewhere else later.
Requiring a field is therefore a statement about the data, and never a way of
tidying its widget. A select carrying inherit offers no empty line anyway,
because inherit is what an empty line would have meant, and one way to say a
thing is enough. Emptiness that no choice carries keeps its empty line: no
venue, the venue's own configuration, offered on every booking channel.
Where the chain runs out, each field's own neutral choice is what applies, and the inherit label says which: "Inherit: A single day", "Inherit: Use the site default". Those choices stay pickable in their own right, because picking one outright and inheriting it are different instructions the moment a level names something else.
Declaring a field¶
Every form-displayable base field of yoyaku_resource is offered, except its
identity: label, status, type and langcode. A module adding a resource
field gets a rule for it with no work, grouped and ordered the way the resource
form groups and orders it, through ResourceFieldGroups.
To say more about a field, that it must end up answered or that it is copied
rather than followed, implement hook_yoyaku_field_rule_info(). Declarations
are merged by id, so a module saying one thing about a field does not have to
restate the rest.
#[Hook('yoyaku_field_rule_info')]
public function hook_yoyaku_field_rule_info(): array {
return [
'yoyaku_resource.my_field' => ['required' => TRUE],
];
}