1
0
Files
re_capitulating_queer/kirby/src/Form/Field.php
Philip Wagner 78b6c0d381 init
2024-08-31 10:01:49 +02:00

511 lines
11 KiB
PHP

<?php
namespace Kirby\Form;
use Closure;
use Exception;
use Kirby\Cms\App;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Component;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\V;
/**
* Form Field object that takes a Vue component style
* array of properties and methods and converts them
* to a usable field option array for the API.
*
* @package Kirby Form
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class Field extends Component
{
/**
* An array of all found errors
*/
protected array|null $errors = null;
/**
* Parent collection with all fields of the current form
*/
protected Fields|null $formFields;
/**
* Registry for all component mixins
*/
public static array $mixins = [];
/**
* Registry for all component types
*/
public static array $types = [];
/**
* @throws \Kirby\Exception\InvalidArgumentException
*/
public function __construct(
string $type,
array $attrs = [],
Fields|null $formFields = null
) {
if (isset(static::$types[$type]) === false) {
throw new InvalidArgumentException([
'key' => 'field.type.missing',
'data' => ['name' => $attrs['name'] ?? '-', 'type' => $type]
]);
}
if (isset($attrs['model']) === false) {
throw new InvalidArgumentException('Field requires a model');
}
$this->formFields = $formFields;
// use the type as fallback for the name
$attrs['name'] ??= $type;
$attrs['type'] = $type;
parent::__construct($type, $attrs);
}
/**
* Returns field api call
*/
public function api(): mixed
{
if (
isset($this->options['api']) === true &&
$this->options['api'] instanceof Closure
) {
return $this->options['api']->call($this);
}
return null;
}
/**
* Returns field data
*/
public function data(bool $default = false): mixed
{
$save = $this->options['save'] ?? true;
if ($default === true && $this->isEmpty($this->value)) {
$value = $this->default();
} else {
$value = $this->value;
}
if ($save === false) {
return null;
}
if ($save instanceof Closure) {
return $save->call($this, $value);
}
return $value;
}
/**
* Default props and computed of the field
*/
public static function defaults(): array
{
return [
'props' => [
/**
* Optional text that will be shown after the input
*/
'after' => function ($after = null) {
return I18n::translate($after, $after);
},
/**
* Sets the focus on this field when the form loads. Only the first field with this label gets
*/
'autofocus' => function (bool $autofocus = null): bool {
return $autofocus ?? false;
},
/**
* Optional text that will be shown before the input
*/
'before' => function ($before = null) {
return I18n::translate($before, $before);
},
/**
* Default value for the field, which will be used when a page/file/user is created
*/
'default' => function ($default = null) {
return $default;
},
/**
* If `true`, the field is no longer editable and will not be saved
*/
'disabled' => function (bool $disabled = null): bool {
return $disabled ?? false;
},
/**
* Optional help text below the field
*/
'help' => function ($help = null) {
return I18n::translate($help, $help);
},
/**
* Optional icon that will be shown at the end of the field
*/
'icon' => function (string $icon = null) {
return $icon;
},
/**
* The field label can be set as string or associative array with translations
*/
'label' => function ($label = null) {
return I18n::translate($label, $label);
},
/**
* Optional placeholder value that will be shown when the field is empty
*/
'placeholder' => function ($placeholder = null) {
return I18n::translate($placeholder, $placeholder);
},
/**
* If `true`, the field has to be filled in correctly to be saved.
*/
'required' => function (bool $required = null): bool {
return $required ?? false;
},
/**
* If `false`, the field will be disabled in non-default languages and cannot be translated. This is only relevant in multi-language setups.
*/
'translate' => function (bool $translate = true): bool {
return $translate;
},
/**
* Conditions when the field will be shown (since 3.1.0)
*/
'when' => function ($when = null) {
return $when;
},
/**
* The width of the field in the field grid. Available widths: `1/1`, `1/2`, `1/3`, `1/4`, `2/3`, `3/4`
*/
'width' => function (string $width = '1/1') {
return $width;
},
'value' => function ($value = null) {
return $value;
}
],
'computed' => [
'after' => function () {
/** @var \Kirby\Form\Field $this */
if ($this->after !== null) {
return $this->model()->toString($this->after);
}
},
'before' => function () {
/** @var \Kirby\Form\Field $this */
if ($this->before !== null) {
return $this->model()->toString($this->before);
}
},
'default' => function () {
/** @var \Kirby\Form\Field $this */
if ($this->default === null) {
return;
}
if (is_string($this->default) === false) {
return $this->default;
}
return $this->model()->toString($this->default);
},
'help' => function () {
/** @var \Kirby\Form\Field $this */
if ($this->help) {
$help = $this->model()->toSafeString($this->help);
$help = $this->kirby()->kirbytext($help);
return $help;
}
},
'label' => function () {
/** @var \Kirby\Form\Field $this */
if ($this->label !== null) {
return $this->model()->toString($this->label);
}
},
'placeholder' => function () {
/** @var \Kirby\Form\Field $this */
if ($this->placeholder !== null) {
return $this->model()->toString($this->placeholder);
}
}
]
];
}
/**
* Returns optional dialog routes for the field
*/
public function dialogs(): array
{
if (
isset($this->options['dialogs']) === true &&
$this->options['dialogs'] instanceof Closure
) {
return $this->options['dialogs']->call($this);
}
return [];
}
/**
* Returns optional drawer routes for the field
*/
public function drawers(): array
{
if (
isset($this->options['drawers']) === true &&
$this->options['drawers'] instanceof Closure
) {
return $this->options['drawers']->call($this);
}
return [];
}
/**
* Creates a new field instance
*/
public static function factory(
string $type,
array $attrs = [],
Fields|null $formFields = null
): static|FieldClass {
$field = static::$types[$type] ?? null;
if (is_string($field) && class_exists($field) === true) {
$attrs['siblings'] = $formFields;
return new $field($attrs);
}
return new static($type, $attrs, $formFields);
}
/**
* Parent collection with all fields of the current form
*/
public function formFields(): Fields|null
{
return $this->formFields;
}
/**
* Validates when run for the first time and returns any errors
*/
public function errors(): array
{
if ($this->errors === null) {
$this->validate();
}
return $this->errors;
}
/**
* Checks if the field is empty
*/
public function isEmpty(mixed ...$args): bool
{
$value = match (count($args)) {
0 => $this->value(),
default => $args[0]
};
if ($empty = $this->options['isEmpty'] ?? null) {
return $empty->call($this, $value);
}
return in_array($value, [null, '', []], true);
}
/**
* Checks if the field is hidden
*/
public function isHidden(): bool
{
return ($this->options['hidden'] ?? false) === true;
}
/**
* Checks if the field is invalid
*/
public function isInvalid(): bool
{
return empty($this->errors()) === false;
}
/**
* Checks if the field is required
*/
public function isRequired(): bool
{
return $this->required ?? false;
}
/**
* Checks if the field is valid
*/
public function isValid(): bool
{
return empty($this->errors()) === true;
}
/**
* Returns the Kirby instance
*/
public function kirby(): App
{
return $this->model()->kirby();
}
/**
* Returns the parent model
*/
public function model(): mixed
{
return $this->model;
}
/**
* Checks if the field needs a value before being saved;
* this is the case if all of the following requirements are met:
* - The field is saveable
* - The field is required
* - The field is currently empty
* - The field is not currently inactive because of a `when` rule
*/
protected function needsValue(): bool
{
// check simple conditions first
if (
$this->save() === false ||
$this->isRequired() === false ||
$this->isEmpty() === false
) {
return false;
}
// check the data of the relevant fields if there is a `when` option
if (
empty($this->when) === false &&
is_array($this->when) === true &&
$formFields = $this->formFields()
) {
foreach ($this->when as $field => $value) {
$field = $formFields->get($field);
$inputValue = $field?->value() ?? '';
// if the input data doesn't match the requested `when` value,
// that means that this field is not required and can be saved
// (*all* `when` conditions must be met for this field to be required)
if ($inputValue !== $value) {
return false;
}
}
}
// either there was no `when` condition or all conditions matched
return true;
}
/**
* Checks if the field is saveable
*/
public function save(): bool
{
return ($this->options['save'] ?? true) !== false;
}
/**
* Converts the field to a plain array
*/
public function toArray(): array
{
$array = parent::toArray();
unset($array['model']);
$array['hidden'] = $this->isHidden();
$array['saveable'] = $this->save();
$array['signature'] = md5(json_encode($array));
ksort($array);
return array_filter(
$array,
fn ($item) => $item !== null && is_object($item) === false
);
}
/**
* Runs the validations defined for the field
*/
protected function validate(): void
{
$validations = $this->options['validations'] ?? [];
$this->errors = [];
// validate required values
if ($this->needsValue() === true) {
$this->errors['required'] = I18n::translate('error.validation.required');
}
foreach ($validations as $key => $validation) {
if (is_int($key) === true) {
// predefined validation
try {
Validations::$validation($this, $this->value());
} catch (Exception $e) {
$this->errors[$validation] = $e->getMessage();
}
continue;
}
if ($validation instanceof Closure) {
try {
$validation->call($this, $this->value());
} catch (Exception $e) {
$this->errors[$key] = $e->getMessage();
}
}
}
if (
empty($this->validate) === false &&
($this->isEmpty() === false || $this->isRequired() === true)
) {
$rules = A::wrap($this->validate);
$errors = V::errors($this->value(), $rules);
if (empty($errors) === false) {
$this->errors = array_merge($this->errors, $errors);
}
}
}
/**
* Returns the value of the field if saveable
* otherwise it returns null
*/
public function value(): mixed
{
return $this->save() ? $this->value : null;
}
}