Skip to content

Commit

Permalink
Improve handling of non-string options
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominic Tubach committed Sep 20, 2024
1 parent 69a3797 commit 45c9d9f
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 109 deletions.
91 changes: 91 additions & 0 deletions src/Form/Control/Callbacks/OptionValueCallbacks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

/*
* Copyright (C) 2024 SYSTOPIA GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace Drupal\json_forms\Form\Control\Callbacks;

use Drupal\Core\Form\FormStateInterface;

final class OptionValueCallbacks {

/**
* @phpstan-param array{
* '#value': scalar,
* '#options': array<string|int, string>,
* '#required'?: bool,
* }&array<int|string, mixed> $element
*/
public static function validate(array $element, FormStateInterface $formState): void {
if (0 === $element['#value'] && isset($element['#options'][0]) && TRUE === ($element['#required'] ?? FALSE)) {
/*
* 0 is treated as empty value by Drupal and violates "#required".
* However if it is in the allowed options we want to accept it as valid
* value. Limit validation errors is reset by Drupal after validating this
* element.
*/
$formState->setLimitValidationErrors([]);
}
}

/**
* @param array<int|string, mixed> $element
* @param mixed $input
* @param \Drupal\Core\Form\FormStateInterface $formState
*
* @return mixed
*/
public static function value(array $element, $input, FormStateInterface $formState) {
if (FALSE === $input) {
$input = $element['#default_value'] ?? NULL;
}

if (NULL === $input) {
// Prevent empty string as value. Drupal sets an empty string in this
// case if no value is set in the form state.
$formState->setValueForElement($element, NULL);

return NULL;
}

if (in_array($input, $element['#_option_values'], TRUE)
|| self::isIntegerish($input) && in_array((int) $input, $element['#_option_values'], TRUE)
) {
$input = $element['#_option_values'][$input];
}

return $input;
}

/**
* @param mixed $value
*
* @return bool
*
* @phpstan-assert-if-true float|string|int $value
*/
private static function isIntegerish($value): bool {
if (\is_string($value)) {
$value = \trim($value);
}

return \is_numeric($value) && $value == (string) $value;
}

}
59 changes: 0 additions & 59 deletions src/Form/Control/Callbacks/RadiosValueCallback.php

This file was deleted.

50 changes: 11 additions & 39 deletions src/Form/Control/Callbacks/SelectCallbacks.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,12 @@ final class SelectCallbacks {
/**
* @phpstan-param array{
* '#value': scalar,
* '#options': array<scalar, string>,
* '#required': bool
* '#options': array<string|int, string>,
* '#required'?: bool
* }&array<int|string, mixed> $element
*/
public static function validate(array $element, FormStateInterface $formState): void {
if (0 === $element['#value'] && isset($element['#options'][0]) && $element['#required']) {
/*
* 0 is treated as empty value by Drupal and violates "#required".
* However if it is in the allowed options we want to accept it as valid
* value. Limit validation errors is reset by Drupal after validating this
* element.
*/
$formState->setLimitValidationErrors([]);
}
OptionValueCallbacks::validate($element, $formState);
}

/**
Expand All @@ -50,39 +42,19 @@ public static function validate(array $element, FormStateInterface $formState):
* @return mixed
*/
public static function value(array &$element, $input, FormStateInterface $formState) {
$value = self::getValue($element, $input, $formState);

if (NULL === $value) {
// Prevent empty string as value. Drupal sets an empty string in this
// case if no value is set in the form state.
$formState->setValueForElement($element, NULL);
}

return $value;
}

/**
* @param array<int|string, mixed> $element
* @param mixed $input
*
* @return mixed
*/
private static function getValue(array &$element, $input, FormStateInterface $formState) {
if (FALSE === $input) {
return $element['#default_value'] ?? NULL;
}

if (array_key_exists('#empty_value', $element) && $input === (string) $element['#empty_value']) {
return '' === $element['#empty_value'] ? NULL : $element['#empty_value'];
}
$input = '' === $element['#empty_value'] ? NULL : $element['#empty_value'];

foreach (array_keys($element['#options']) as $option) {
if ($input === (string) $option) {
return $option;
if (NULL === $input) {
// Prevent empty string as value. Drupal sets an empty string in this
// case if no value is set in the form state.
$formState->setValueForElement($element, NULL);
}

return $input;
}

return $input;
return OptionValueCallbacks::value($element, $input, $formState);
}

}
1 change: 1 addition & 0 deletions src/Form/Control/CheckboxesArrayFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public function createFormArray(
/** @var \Drupal\json_forms\JsonForms\Definition\Control\ArrayControlDefinition $definition */
$form = [
'#type' => 'checkboxes',
// @todo Handle non-string values and integerish strings.
'#options' => OptionsBuilder::buildOptions($definition),
] + BasicFormPropertiesFactory::createFieldProperties($definition, $formState);

Expand Down
15 changes: 10 additions & 5 deletions src/Form/Control/RadiosArrayFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
use Assert\Assertion;
use Drupal\Core\Form\FormStateInterface;
use Drupal\json_forms\Form\AbstractConcreteFormArrayFactory;
use Drupal\json_forms\Form\Control\Callbacks\RadiosValueCallback;
use Drupal\json_forms\Form\Control\Callbacks\OptionValueCallbacks;
use Drupal\json_forms\Form\Control\Util\BasicFormPropertiesFactory;
use Drupal\json_forms\Form\Control\Util\OptionsBuilder;
use Drupal\json_forms\Form\FormArrayFactoryInterface;
Expand All @@ -50,15 +50,20 @@ public function createFormArray(
$form = [
'#type' => 'radios',
'#options' => OptionsBuilder::buildOptions($definition),
'#value_callback' => RadiosValueCallback::class . '::convert',
'#_option_values' => OptionsBuilder::buildOptionValues($definition),
'#value_callback' => OptionValueCallbacks::class . '::value',
'#element_validate' => [OptionValueCallbacks::class . '::validate'],
'#_type' => $definition->getType(),
] + BasicFormPropertiesFactory::createFieldProperties($definition, $formState);

if ('boolean' === $form['#_type'] && is_bool($form['#default_value'] ?? NULL)) {
// If default value is actual a boolean, the corresponding radio is not
// selected.
if (is_bool($form['#default_value'] ?? NULL)) {
// If default value is a boolean, the corresponding radio is not selected.
$form['#default_value'] = $form['#default_value'] ? '1' : '0';
}
elseif (0 === ($form['#default_value'] ?? NULL)) {
// If default value is 0, the corresponding radio is not selected.
$form['#default_value'] = '0';
}

return $form;
}
Expand Down
1 change: 1 addition & 0 deletions src/Form/Control/SelectArrayFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function createFormArray(
$form = [
'#type' => 'select',
'#options' => OptionsBuilder::buildOptions($definition),
'#_option_values' => OptionsBuilder::buildOptionValues($definition),
'#value_callback' => SelectCallbacks::class . '::value',
'#element_validate' => [SelectCallbacks::class . '::validate'],
] + BasicFormPropertiesFactory::createFieldProperties($definition, $formState);
Expand Down
48 changes: 42 additions & 6 deletions src/Form/Control/Util/OptionsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,50 @@ final class OptionsBuilder {
/**
* @param \Drupal\json_forms\JsonForms\Definition\Control\ControlDefinition $definition
*
* @return array<scalar, string> Options for radio buttons or select fields.
* @return array<string|int, string>
* Options for radio buttons or select fields. (Mapping of option value as
* string to label. Integerish strings are treated as integer when used as
* array key.)
*/
public static function buildOptions(ControlDefinition $definition): array {
$options = [];
foreach (self::getEnum($definition) as $enum) {
if (NULL !== $enum) {
$options[$enum] = (string) $enum;
$options[self::optionToString($enum)] = (string) $enum;
}
}

foreach (self::getOneOf($definition) as $option) {
if (\property_exists($option, 'const')) {
if (NULL !== $option->const) {
$options[$option->const] = $option->title ?? (string) $option->const;
}
if (\property_exists($option, 'const') && NULL !== $option->const) {
$options[self::optionToString($option->const)] = $option->title ?? (string) $option->const;
}
}

return $options;
}

/**
* @return array<string|int, scalar>
* Mapping of option value as string to actual option value. Integerish
* strings are treated as integer when used as array key.
*/
public static function buildOptionValues(ControlDefinition $definition): array {
$optionValues = [];
foreach (self::getEnum($definition) as $enum) {
if (NULL !== $enum) {
$optionValues[self::optionToString($enum)] = $enum;
}
}

foreach (self::getOneOf($definition) as $option) {
if (\property_exists($option, 'const') && NULL !== $option->const) {
$optionValues[self::optionToString($option->const)] = $option->const;
}
}

return $optionValues;
}

/**
* @phpstan-return array<scalar|null>
*/
Expand Down Expand Up @@ -82,4 +105,17 @@ private static function getOneOf(ControlDefinition $definition): array {
return $definition->getOneOf() ?? [];
}

/**
* Because the options are used in a HTML form we convert them to strings.
*
* @param scalar $option
*/
private static function optionToString($option): string {
if (FALSE === $option) {
return '0';
}

return (string) $option;
}

}

0 comments on commit 45c9d9f

Please sign in to comment.