Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting minimum and maximum count of selected #96

Merged
merged 12 commits into from
Oct 30, 2023
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* (feature) Extract parsing of `_editable` data into `PreviewDataParser`.
* (internal) Expose `_editable` data via `StoryMetaData::getPreviewData()`.
* (feature) Add `ComponentPreviewData` helper to easily render component meta and preview data.
* (improvement) Add support for setting minimum and maximum count of selected `ChoiceField` options.


3.2.2
Expand Down
9 changes: 8 additions & 1 deletion src/Context/ComponentContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Torr\Storyblok\Context;

use Psr\Log\LoggerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Contracts\Service\Attribute\Required;
use Torr\Storyblok\Api\Transformer\StoryblokIdSlugMapper;
use Torr\Storyblok\Component\AbstractComponent;
Expand All @@ -12,7 +13,10 @@
use Torr\Storyblok\Transformer\DataTransformer;
use Torr\Storyblok\Validator\DataValidator;

final class ComponentContext
/**
* @final
*/
class ComponentContext
{
public ?StoryblokIdSlugMapper $storyblokIdSlugMapper = null;

Expand All @@ -39,6 +43,9 @@ public function setStoryblokIdSlugMapper (StoryblokIdSlugMapper $storyblokIdSlug

/**
* @see DataValidator::ensureDataIsValid()
*
* @param string[] $contentPath
* @param array<Constraint|null> $constraints
*/
public function ensureDataIsValid (
array $contentPath,
Expand Down
139 changes: 126 additions & 13 deletions src/Field/Definition/ChoiceField.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
use Symfony\Component\Validator\Constraints\Count;
use Symfony\Component\Validator\Constraints\IsNull;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Type;
use Torr\Storyblok\Context\ComponentContext;
use Torr\Storyblok\Exception\InvalidFieldConfigurationException;
use Torr\Storyblok\Field\Choices\ChoicesInterface;
use Torr\Storyblok\Field\FieldType;
use Torr\Storyblok\Visitor\DataVisitorInterface;
Expand All @@ -20,9 +23,29 @@ public function __construct (
private readonly ChoicesInterface $choices,
private readonly bool $allowMultiselect = false,
private readonly int|string|\BackedEnum|null $defaultValue = null,
private readonly ?int $minimumNumberOfOptions = null,
private readonly ?int $maximumNumberOfOptions = null,
)
{
parent::__construct($label, $this->defaultValue);

if (!$this->allowMultiselect && (null !== $this->minimumNumberOfOptions || null !== $this->maximumNumberOfOptions))
{
throw new InvalidFieldConfigurationException(\sprintf(
"Can't configure minimum or maximum amount of options for single-select choice.",
));
}

if (
null !== $this->minimumNumberOfOptions
&& null !== $this->maximumNumberOfOptions
&& $this->minimumNumberOfOptions > $this->maximumNumberOfOptions
)
{
throw new InvalidFieldConfigurationException(\sprintf(
"The minimum number of options value can't be higher than the maximum",
));
}
}


Expand All @@ -48,6 +71,8 @@ protected function toManagementApiData () : array
"default_value" => $this->defaultValue instanceof \BackedEnum
? $this->defaultValue->value
: $this->defaultValue,
"min_options" => $this->minimumNumberOfOptions,
"max_options" => $this->maximumNumberOfOptions,
],
);
}
Expand All @@ -57,40 +82,128 @@ protected function toManagementApiData () : array
*/
public function validateData (ComponentContext $context, array $contentPath, mixed $data, array $fullData) : void
{
$allowedValueTypeConstraints = new AtLeastOneOf([
new Type("string"),
new Type("int"),
]);
$context->ensureDataIsValid(
$contentPath,
$this,
$data,
[
new AtLeastOneOf([
new Type("string"),
new Type("int"),
new Type("array"),
new IsNull(),
]),
],
);

if ($this->allowMultiselect)
{
$this->validateMultiSelect($context, $contentPath, $data);
}
else
{
$this->validateSingleSelect($context, $contentPath, $data);
}
}

/**
*/
private function validateSingleSelect (ComponentContext $context, array $contentPath, mixed $data) : void
{
$context->ensureDataIsValid(
$contentPath,
$this,
$data,
[
$this->allowMultiselect
? new All([new NotNull(), $allowedValueTypeConstraints])
: $allowedValueTypeConstraints,
new AtLeastOneOf([
new Type("string"),
new Type("int"),
new IsNull(),
]),
],
);

\assert(null === $data || \is_array($data) || \is_int($data) || \is_string($data));
\assert(null === $data || \is_string($data) || \is_int($data));

if (\is_string($data))
$data = $context->normalizeOptionalString((string) $data);

$context->ensureDataIsValid(
$contentPath,
$this,
$data,
[
$this->required
? new NotNull()
: null,
],
);

if (null === $data)
{
$data = $context->normalizeOptionalString($data);
return;
}

$choicesConstraints = $this->choices->getValidationConstraints($this->allowMultiselect);
$context->ensureDataIsValid(
$contentPath,
$this,
$data,
$this->choices->getValidationConstraints(false),
);
}


if (null !== $data && !empty($choicesConstraints))
/**
*/
private function validateMultiSelect (ComponentContext $context, array $contentPath, mixed $data) : void
{
// first validate basic structure
if (null !== $data)
{
// if we get a non-null-value, we assert that it is an array full of ints / strings
$context->ensureDataIsValid(
$contentPath,
$this,
$data,
$choicesConstraints,
[
new Type("array"),
new All([
new NotNull(),
new AtLeastOneOf([
new Type("string"),
new Type("int"),
]),
]),
],
);
}

\assert(null === $data || \is_array($data));

$data = \array_map(
static fn (mixed $value) => $context->normalizeOptionalString((string) $value),
// we are in a multiselect, so we expect an array
$data ?? [],
);

// collect constraints for content
$constraints = $this->choices->getValidationConstraints(true);

if ($this->required || null !== $this->minimumNumberOfOptions || null !== $this->maximumNumberOfOptions)
{
$constraints[] = new Count(
min: $this->minimumNumberOfOptions ?? ($this->required ? 1 : null),
max: $this->maximumNumberOfOptions,
minMessage: "At least {{ limit }} option(s) must be selected.",
maxMessage: "You cannot specify more than {{ limit }} options.",
);
}

$context->ensureDataIsValid(
$contentPath,
$this,
$data,
$constraints,
);
}

/**
Expand Down
Loading