* @link https://getkirby.com * @copyright Bastian Allgeier * @license https://opensource.org/licenses/MIT */ class Field extends Component { /** * An array of all found errors * * @var array|null */ protected $errors; /** * Parent collection with all fields of the current form * * @var \Kirby\Form\Fields|null */ protected $formFields; /** * Registry for all component mixins * * @var array */ public static $mixins = []; /** * Registry for all component types * * @var array */ public static $types = []; /** * Field constructor * * @param string $type * @param array $attrs * @param \Kirby\Form\Fields|null $formFields * @throws \Kirby\Exception\InvalidArgumentException */ public function __construct(string $type, array $attrs = [], ?Fields $formFields = null) { if (isset(static::$types[$type]) === false) { throw new InvalidArgumentException('The field type "' . $type . '" does not exist'); } 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 * * @return mixed */ public function api() { if ( isset($this->options['api']) === true && is_a($this->options['api'], 'Closure') === true ) { return $this->options['api']->call($this); } } /** * Returns field data * * @param bool $default * @return mixed */ public function data(bool $default = false) { $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 (is_a($save, 'Closure') === true) { return $save->call($this, $value); } return $value; } /** * Default props and computed of the field * * @return array */ 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); } } ] ]; } /** * Creates a new field instance * * @param string $type * @param array $attrs * @param Fields|null $formFields * @return static */ public static function factory(string $type, array $attrs = [], ?Fields $formFields = null) { $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 * * @return \Kirby\Form\Fields|null */ public function formFields(): ?Fields { return $this->formFields; } /** * Validates when run for the first time and returns any errors * * @return array */ public function errors(): array { if ($this->errors === null) { $this->validate(); } return $this->errors; } /** * Checks if the field is empty * * @param mixed ...$args * @return bool */ public function isEmpty(...$args): bool { if (count($args) === 0) { $value = $this->value(); } else { $value = $args[0]; } if (isset($this->options['isEmpty']) === true) { return $this->options['isEmpty']->call($this, $value); } return in_array($value, [null, '', []], true); } /** * Checks if the field is invalid * * @return bool */ public function isInvalid(): bool { return empty($this->errors()) === false; } /** * Checks if the field is required * * @return bool */ public function isRequired(): bool { return $this->required ?? false; } /** * Checks if the field is valid * * @return bool */ public function isValid(): bool { return empty($this->errors()) === true; } /** * Returns the Kirby instance * * @return \Kirby\Cms\App */ public function kirby() { return $this->model()->kirby(); } /** * Returns the parent model * * @return mixed */ public function model() { 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 * * @return bool */ 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(); if ($formFields !== null) { foreach ($this->when as $field => $value) { $field = $formFields->get($field); $inputValue = $field !== null ? $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 * * @return bool */ public function save(): bool { return ($this->options['save'] ?? true) !== false; } /** * Converts the field to a plain array * * @return array */ public function toArray(): array { $array = parent::toArray(); unset($array['model']); $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 * * @return void */ 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 (is_a($validation, 'Closure') === true) { 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 * * @return mixed */ public function value() { return $this->save() ? $this->value : null; } }