Skip to content

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],
  ];
}